diff --git a/src/rand.cpp b/src/rand.cpp index df3bab956b..43a63cdb35 100644 --- a/src/rand.cpp +++ b/src/rand.cpp @@ -19,117 +19,139 @@ #include "rand.h" #include "utils.h" #include "output.h" -#include "compiler.h" #include #include -#include #include #include -namespace { -Rand::RNG rng; +namespace Rand +{ + AbstractRNGWrapper::result_type SequencedRNGWrapper::operator()() + { + return pickNext(); + } + + AbstractRNGWrapper::result_type SequencedRNGWrapper::distribute(std::uint32_t max) + { + return Utils::Clamp(pickNext(), 0u, max); + } -/** Gets a random number uniformly distributed in [0, U32_MAX] */ -uint32_t GetRandomU32() { return rng(); } + std::int32_t SequencedRNGWrapper::distribute(std::int32_t from, std::int32_t to) + { + return Utils::Clamp(static_cast(pickNext()), from, to); + } + + void SequencedRNGWrapper::seed(Seed_t seed) + { + } -int32_t rng_lock_value = 0; -bool rng_locked= false; + AbstractRNGWrapper::result_type SequencedRNGWrapper::pickNext() + { + auto value = m_Sequence[m_NextIndex]; + m_NextIndex = ++m_NextIndex % std::size(m_Sequence); + return value; + } } -/** Generate a random number in the range [0,max] */ -static uint32_t GetRandomUnsigned(uint32_t max) +template +class RNGWrapper : + public Rand::AbstractRNGWrapper { - if (max == 0xffffffffull) return GetRandomU32(); - - // Rejection sampling: - // 1. Divide the range of uint32 into blocks of max+1 - // numbers each, with rem numbers left over. - // 2. Generate a random u32. If it belongs to a block, - // mod it into the range [0,max] and accept it. - // 3. If it fell into the range of rem leftover numbers, - // reject it and go back to step 2. - uint32_t m = max + 1; - uint32_t rem = -m % m; // = 2^32 mod m - while (true) { - uint32_t n = GetRandomU32(); - if (n >= rem) - return n % m; +public: + explicit RNGWrapper(TGenerator generator = TGenerator{}) : + AbstractRNGWrapper{}, + m_Generator{ std::move(generator) } + { } -} -int32_t Rand::GetRandomNumber(int32_t from, int32_t to) { - assert(from <= to); - if (rng_locked) { - return Utils::Clamp(rng_lock_value, from, to); + void seed(Rand::Seed_t seed) override + { + m_Generator.seed(seed); } - // Don't use uniform_int_distribution--the algorithm used isn't - // portable between stdlibs. - // We do from + (rand int in [0, to-from]). The miracle of two's - // complement let's us do this all in unsigned and then just cast - // back. - uint32_t ufrom = uint32_t(from); - uint32_t uto = uint32_t(to); - uint32_t urange = uto - ufrom; - uint32_t ures = ufrom + GetRandomUnsigned(urange); - return int32_t(ures); + + /** Gets a random number uniformly distributed in [0, U32_MAX] */ + result_type operator()() override + { + return m_Generator(); + } + + /** Generate a random number in the range [0,max] */ + result_type distribute(std::uint32_t max) override + { + if (max == 0xffffffffull) + { + return m_Generator(); + } + + // Rejection sampling: + // 1. Divide the range of uint32 into blocks of max+1 + // numbers each, with rem numbers left over. + // 2. Generate a random u32. If it belongs to a block, + // mod it into the range [0,max] and accept it. + // 3. If it fell into the range of rem leftover numbers, + // reject it and go back to step 2. + std::uint32_t m = max + 1; + std::uint32_t rem = -m % m; // = 2^32 mod m + while (true) { + auto n = m_Generator(); + if (n >= rem) + return n % m; + } + } + + std::int32_t distribute(std::int32_t from, std::int32_t to) override + { + // Don't use uniform_int_distribution--the algorithm used isn't + // portable between stdlibs. + // We do from + (rand int in [0, to-from]). The miracle of two's + // complement let's us do this all in unsigned and then just cast + // back. + auto ufrom = static_cast(from); + auto uto = static_cast(to); + auto urange = uto - ufrom; + auto ures = ufrom + distribute(urange); + return static_cast(ures); + } + +private: + TGenerator m_Generator; +}; + +namespace { + Rand::RngPtr rng = std::make_unique>(); +} + +Rand::RngPtr Rand::ExchangeRNG(RngPtr newRng) +{ + assert(newRng && "rng must not be nullptr"); + return std::exchange(rng, std::move(newRng)); +} + +std::int32_t Rand::GetRandomNumber(std::int32_t from, std::int32_t to) { + assert(from <= to && "from must be less-equal than to"); + return GetRNG().distribute(from, to); } Rand::RNG& Rand::GetRNG() { - return rng; + assert(rng && "rng is empty"); + return *rng; } -bool Rand::ChanceOf(int32_t n, int32_t m) { +bool Rand::ChanceOf(std::int32_t n, std::int32_t m) { assert(n >= 0 && m > 0); return GetRandomNumber(1, m) <= n; } bool Rand::PercentChance(float rate) { constexpr auto scale = 0x1000000; - return GetRandomNumber(0, scale-1) < int32_t(rate * scale); + return GetRandomNumber(0, scale-1) < static_cast(rate * scale); } bool Rand::PercentChance(int rate) { return GetRandomNumber(0, 99) < rate; } -void Rand::SeedRandomNumberGenerator(int32_t seed) { - rng.seed(seed); +void Rand::SeedRandomNumberGenerator(Seed_t seed) { + GetRNG().seed(seed); Output::Debug("Seeded the RNG with {}.", seed); } - -void Rand::LockRandom(int32_t value) { - rng_locked = true; - rng_lock_value = value; -} - -void Rand::UnlockRandom() { - rng_locked = false; -} - -std::pair Rand::GetRandomLocked() { - return { rng_locked, rng_lock_value }; -} - -Rand::LockGuard::LockGuard(int32_t lock_value, bool locked) { - auto p = GetRandomLocked(); - _prev_locked = p.first; - _prev_lock_value = p.second; - _active = true; - - if (locked) { - LockRandom(lock_value); - } else { - UnlockRandom(); - } -} - -void Rand::LockGuard::Release() noexcept { - if (Enabled()) { - if (_prev_locked) { - LockRandom(_prev_lock_value); - } else { - UnlockRandom(); - } - Dismiss(); - } -} diff --git a/src/rand.h b/src/rand.h index 03c7b76c35..3e9f5bcbdc 100644 --- a/src/rand.h +++ b/src/rand.h @@ -19,19 +19,77 @@ #define EP_RANDOM_H #include -#include -#include #include #include -#include "system.h" #include "string_view.h" #include "span.h" namespace Rand { -/** + +using Seed_t = std::uint32_t; + +class AbstractRNGWrapper +{ +public: + using result_type = std::uint32_t; + + virtual ~AbstractRNGWrapper() noexcept = default; + + AbstractRNGWrapper(const AbstractRNGWrapper&) = delete; + AbstractRNGWrapper& operator =(const AbstractRNGWrapper&) = delete; + AbstractRNGWrapper(AbstractRNGWrapper&&) = delete; + AbstractRNGWrapper& operator =(AbstractRNGWrapper&&) = delete; + + virtual result_type operator()() = 0; + virtual result_type distribute(std::uint32_t max) = 0; + virtual std::int32_t distribute(std::int32_t from, std::int32_t to) = 0; + virtual void seed(Seed_t seed) = 0; + + static result_type min() { return std::numeric_limits::min(); } + static result_type max() { return std::numeric_limits::max(); } + +protected: + AbstractRNGWrapper() noexcept = default; +}; + +class SequencedRNGWrapper : + public AbstractRNGWrapper +{ +public: + explicit SequencedRNGWrapper(std::vector seq) noexcept : + AbstractRNGWrapper{}, + m_Sequence{ std::move(seq) } + { + assert(!std::empty(m_Sequence) && "Empty sequence is not allowed."); + } + + template , result_type>>> + explicit SequencedRNGWrapper(TArgs ... args) noexcept : + SequencedRNGWrapper{ std::vector{ static_cast(args)... } } + { + } + + result_type operator()() override; + + result_type distribute(std::uint32_t max) override; + std::int32_t distribute(std::int32_t from, std::int32_t to) override; + + void seed(Seed_t seed) override; + +private: + std::size_t m_NextIndex = 0; + std::vector m_Sequence; + + result_type pickNext(); +}; + + /** * The random number generator object to use */ -using RNG = std::mt19937; +using RNG = AbstractRNGWrapper; +using RngPtr = std::unique_ptr; + +RngPtr ExchangeRNG(RngPtr newRng); /** * Gets a random number in the inclusive range from - to. @@ -40,7 +98,7 @@ using RNG = std::mt19937; * @param to Interval end * @return Random number in inclusive interval */ -int32_t GetRandomNumber(int32_t from, int32_t to); +std::int32_t GetRandomNumber(std::int32_t from, std::int32_t to); /** * Gets the seeded Random Number Generator (RNG). @@ -56,7 +114,7 @@ RNG& GetRNG(); * @param m denominator of the probability (positive) * @return true with probability n/m, false with probability 1-n/m */ -bool ChanceOf(int32_t n, int32_t m); +bool ChanceOf(std::int32_t n, std::int32_t m); /** * Rolls a random number in [0.0f, 1.0f) returns true if it's less than rate. @@ -80,86 +138,48 @@ bool PercentChance(long rate); * * @param seed Seed to use */ -void SeedRandomNumberGenerator(int32_t seed); - -/** - * Forces GetRandomNumber() and all dervative functions to return a fixed value. - * Useful for testing. - * - * @param lock_value the value to set. A calls to GetRandomNumber(a, b) will return clamp(lock_value, a, b) - * @post All calls to GetRandomNumber(a, b) will return clamp(lock_value, a, b) - */ -void LockRandom(int32_t lock_value); - -/** - * Disables locked random number and returns RNG to original state. - * @post All calls to GetRandomNumber(a, b) will return random values. - */ -void UnlockRandom(); - -/** - * Retrive whether random numbers are locked and if so, which value they are locked to. - * @return whether or not random numbers are locked and if so, to what value. - */ -std::pair GetRandomLocked(); - -/** An RAII guard which fixes the rng while active and resets on destruction */ -class LockGuard { -public: - /** - * Store current state and set locked state - * @param lock_value The rng value to fix to - * @param locked Whether to fix or reset - */ - LockGuard(int32_t lock_value, bool locked = true); - - LockGuard(const LockGuard&) = delete; - LockGuard& operator=(const LockGuard&) = delete; - - /** Move other LockGuard to this */ - LockGuard(LockGuard&& o) noexcept; - LockGuard& operator=(LockGuard&&) = delete; - - /** Calls Release() */ - ~LockGuard(); - - /** If Enabled(), returns the rng locked state to what it was */ - void Release() noexcept; - - /** Disables the LockGuard leaving the rng state as is */ - void Dismiss(); - - /** @return whether the guard is enabled and will release on destruction */ - bool Enabled() const; -private: - int32_t _prev_lock_value = 0; - bool _prev_locked = false; - bool _active = false; -}; - -inline bool PercentChance(long rate) { - return PercentChance(static_cast(rate)); -} - -inline LockGuard::LockGuard(LockGuard&& o) noexcept { - std::swap(_prev_lock_value, o._prev_lock_value); - std::swap(_prev_locked, o._prev_locked); - std::swap(_active, o._active); +void SeedRandomNumberGenerator(Seed_t seed); + +namespace test +{ + template >> + class ScopedRNGExchange + { + public: + template >> + explicit ScopedRNGExchange(TArgs&&... args) : + m_PreviousRNG{ Rand::ExchangeRNG(std::make_unique(std::forward(args)...)) } + { + } + + ~ScopedRNGExchange() noexcept + { + if (m_PreviousRNG) + { + ExchangeRNG(std::move(m_PreviousRNG)); + } + } + + ScopedRNGExchange(const ScopedRNGExchange&) = delete; + ScopedRNGExchange& operator =(const ScopedRNGExchange&) = delete; + + ScopedRNGExchange(ScopedRNGExchange&&) noexcept = default; + ScopedRNGExchange& operator =(ScopedRNGExchange&&) noexcept = default; + + private: + RngPtr m_PreviousRNG; + }; + + template + std::enable_if_t< + std::is_base_of_v && std::is_constructible_v, + ScopedRNGExchange // actual return type + > + makeScopedRNGExchange(TArgs&&... args) + { + return ScopedRNGExchange{ std::forward(args)... }; + } } - -inline LockGuard::~LockGuard() { - Release(); -} - -inline void LockGuard::Dismiss() { - _active = false; -} - -inline bool LockGuard::Enabled() const { - return _active; -} - } // namespace Rand - #endif diff --git a/tests/algo.cpp b/tests/algo.cpp index 00138bb1eb..855782f7d0 100644 --- a/tests/algo.cpp +++ b/tests/algo.cpp @@ -121,13 +121,13 @@ TEST_CASE("Variance") { SUBCASE(">0") { SUBCASE("max") { - Rand::LockGuard lk(INT32_MAX); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::max()); for (int var = 1; var <= 10; ++var) { REQUIRE_EQ(100 + 5 * var, Algo::VarianceAdjustEffect(100, var)); } } SUBCASE("min") { - Rand::LockGuard lk(INT32_MIN); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::min()); for (int var = 1; var <= 10; ++var) { REQUIRE_EQ(100 - 5 * var, Algo::VarianceAdjustEffect(100, var)); } @@ -136,11 +136,11 @@ TEST_CASE("Variance") { SUBCASE("one") { SUBCASE("max") { - Rand::LockGuard lk(INT32_MAX); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::max()); REQUIRE_EQ(2, Algo::VarianceAdjustEffect(1, 10)); } SUBCASE("min") { - Rand::LockGuard lk(INT32_MIN); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::min()); REQUIRE_EQ(1, Algo::VarianceAdjustEffect(1, 10)); } } @@ -159,12 +159,12 @@ TEST_CASE("Variance") { REQUIRE(Player::IsLegacy()); SUBCASE("max") { - Rand::LockGuard lk(INT32_MAX); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::max()); REQUIRE_EQ(0, Algo::VarianceAdjustEffect(0, 0)); REQUIRE_EQ(1, Algo::VarianceAdjustEffect(0, 1)); } SUBCASE("min") { - Rand::LockGuard lk(INT32_MIN); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::min()); REQUIRE_EQ(0, Algo::VarianceAdjustEffect(0, 0)); REQUIRE_EQ(0, Algo::VarianceAdjustEffect(0, 1)); } @@ -909,11 +909,11 @@ TEST_CASE("NormalAttackVariance") { REQUIRE_EQ(target.GetDef(), 90); SUBCASE("max") { - Rand::LockGuard lk(INT32_MAX); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::max()); REQUIRE_EQ(46, Algo::CalcNormalAttackEffect(source, target, Game_Battler::WeaponAll, false, true, lcf::rpg::System::BattleCondition_none, false)); } SUBCASE("min") { - Rand::LockGuard lk(INT32_MIN); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::min()); REQUIRE_EQ(31, Algo::CalcNormalAttackEffect(source, target, Game_Battler::WeaponAll, false, true, lcf::rpg::System::BattleCondition_none, false)); } } @@ -928,12 +928,12 @@ static void testSkillVar(Game_Battler& source, Game_Battler& target, int var, in skill2->scope = lcf::rpg::Skill::Scope_ally; SUBCASE("max") { - Rand::LockGuard lk(INT32_MAX); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::max()); REQUIRE_EQ(dmg_high, Algo::CalcSkillEffect(source, target, *skill1, true)); REQUIRE_EQ(heal_high, Algo::CalcSkillEffect(source, target, *skill2, true)); } SUBCASE("min") { - Rand::LockGuard lk(INT32_MIN); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::min()); REQUIRE_EQ(dmg_low, Algo::CalcSkillEffect(source, target, *skill1, true)); REQUIRE_EQ(heal_low, Algo::CalcSkillEffect(source, target, *skill2, true)); } @@ -965,11 +965,11 @@ TEST_CASE("SelfDestructVariance") { auto source = MakeStatEnemy(1, 150, 0, 0); SUBCASE("max") { - Rand::LockGuard lk(INT32_MAX); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::max()); REQUIRE_EQ(120, Algo::CalcSelfDestructEffect(source, target, true)); } SUBCASE("min") { - Rand::LockGuard lk(INT32_MIN); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::min()); REQUIRE_EQ(80, Algo::CalcSelfDestructEffect(source, target, true)); } } diff --git a/tests/autobattle.cpp b/tests/autobattle.cpp index 2b8ebb5a35..7d64ca67c5 100644 --- a/tests/autobattle.cpp +++ b/tests/autobattle.cpp @@ -46,11 +46,11 @@ TEST_CASE("NormalAttackTargetRank") { REQUIRE_EQ(target.GetDef(), 90); SUBCASE("max") { - Rand::LockGuard lk(INT32_MAX); + auto scoped = Rand::test::makeScopedRNGExchange(static_cast(std::numeric_limits::max())); testNormalAttack(source, target, 1.131, 1.131, 1.158, 1.158); } SUBCASE("min") { - Rand::LockGuard lk(INT32_MIN); + auto scoped = Rand::test::makeScopedRNGExchange(static_cast(std::numeric_limits::min())); testNormalAttack(source, target, 0.141, 0.141, 0.114, 0.114); } } diff --git a/tests/enemyai.cpp b/tests/enemyai.cpp index 1466593f3d..5bc1f07e0e 100644 --- a/tests/enemyai.cpp +++ b/tests/enemyai.cpp @@ -244,7 +244,7 @@ static void testActionType(int start, int end, Game_BattleAlgorithm::Type type_c bool bugs = true; CAPTURE(bugs); - Rand::LockGuard lk(rng); + auto scoped = Rand::test::makeScopedRNGExchange(rng); bugs = true; source.SetBattleAlgorithm(nullptr); EnemyAi::SelectEnemyAiActionRpgRtCompat(source, bugs); @@ -265,7 +265,7 @@ static void testActionNullptr(int start, int end, Game_Enemy& source) { bool bugs = true; CAPTURE(bugs); - Rand::LockGuard lk(rng); + auto scoped = Rand::test::makeScopedRNGExchange(rng); bugs = true; source.SetBattleAlgorithm(nullptr); EnemyAi::SelectEnemyAiActionRpgRtCompat(source, bugs); diff --git a/tests/game_player_input.cpp b/tests/game_player_input.cpp index 6f2cb420cd..6dc121bdae 100644 --- a/tests/game_player_input.cpp +++ b/tests/game_player_input.cpp @@ -67,7 +67,7 @@ static void testMove(bool success, int input_dir, int dir, int x, int y, int dx, const auto ty = y + dy; // Force RNG to always fail to generate a random encounter - Rand::LockGuard lk(INT32_MAX); + auto scoped = Rand::test::makeScopedRNGExchange(std::numeric_limits::max()); ForceUpdate(ch); if (success) { int enc_steps = (cheat && debug) ? 0 : 100; diff --git a/tests/rand.cpp b/tests/rand.cpp index 361d672252..4200f40c1a 100644 --- a/tests/rand.cpp +++ b/tests/rand.cpp @@ -19,73 +19,66 @@ TEST_CASE("GetRandomNumber") { testGetRandomNumber(-5, -2); } -TEST_CASE("Lock") { - REQUIRE_FALSE(Rand::GetRandomLocked().first); - - Rand::LockRandom(55); - - REQUIRE(Rand::GetRandomLocked().first); - REQUIRE_EQ(Rand::GetRandomLocked().second, 55); - - Rand::UnlockRandom(); - - REQUIRE_FALSE(Rand::GetRandomLocked().first); +static void testSequenceGenerator(const std::vector& seq, Rand::SequencedRNGWrapper& rng, std::size_t iterations = 1) +{ + for (std::size_t i = 0; i < iterations; ++i) + { + for (auto value : seq) + { + REQUIRE(rng() == value); + } + } } -TEST_CASE("LockGuardNest") { - REQUIRE_FALSE(Rand::GetRandomLocked().first); - { - Rand::LockGuard fg(32); - REQUIRE(fg.Enabled()); +TEST_CASE("Single-Element Sequence") { - REQUIRE(Rand::GetRandomLocked().first); - REQUIRE_EQ(Rand::GetRandomLocked().second, 32); + std::vector seq{ 42 }; + Rand::SequencedRNGWrapper seqRNG(seq); - { - Rand::LockGuard fg(0, false); - REQUIRE(fg.Enabled()); + testSequenceGenerator(seq, seqRNG, 3); +} - REQUIRE_FALSE(Rand::GetRandomLocked().first); - } +TEST_CASE("Multi-Element Sequence") { + + std::vector seq{ 42, 1337, 42, 3, 1, 3 }; + Rand::SequencedRNGWrapper seqRNG(seq); - REQUIRE(Rand::GetRandomLocked().first); - REQUIRE_EQ(Rand::GetRandomLocked().second, 32); - } - REQUIRE_FALSE(Rand::GetRandomLocked().first); + testSequenceGenerator(seq, seqRNG, 3); } -TEST_CASE("LockGuardRelease") { - REQUIRE_FALSE(Rand::GetRandomLocked().first); +TEST_CASE("GetRNG") { - Rand::LockGuard fg(-16); - REQUIRE(fg.Enabled()); + REQUIRE(&Rand::GetRNG() != nullptr); +} - REQUIRE(Rand::GetRandomLocked().first); - REQUIRE_EQ(Rand::GetRandomLocked().second, -16); +TEST_CASE("ExchangeRNG") { - fg.Release(); + auto* preRNGPtr = &Rand::GetRNG(); + std::vector seq{ 42, 1337, 42, 3, 1, 3 }; + auto seqRNG = std::make_unique(seq); + auto* seqRNGPtr = seqRNG.get(); + auto prevRNG = Rand::ExchangeRNG(std::move(seqRNG)); + + REQUIRE(preRNGPtr == prevRNG.get()); - REQUIRE_FALSE(Rand::GetRandomLocked().first); - REQUIRE_FALSE(fg.Enabled()); + REQUIRE(seqRNGPtr == &Rand::GetRNG()); } -TEST_CASE("LockGuardDismiss") { - Rand::LockGuard fg(INT32_MAX); - REQUIRE(fg.Enabled()); - - REQUIRE(Rand::GetRandomLocked().first); - REQUIRE_EQ(Rand::GetRandomLocked().second, INT32_MAX); +TEST_CASE("ScopedRNGExchange") { - fg.Dismiss(); + auto* preRNGPtr = &Rand::GetRNG(); + + { + auto scoped = Rand::test::makeScopedRNGExchange(1); - REQUIRE(Rand::GetRandomLocked().first); - REQUIRE_EQ(Rand::GetRandomLocked().second, INT32_MAX); + REQUIRE(preRNGPtr != &Rand::GetRNG()); + } - Rand::UnlockRandom(); + REQUIRE(preRNGPtr == &Rand::GetRNG()); } static void testGetRandomNumberFixed(int32_t a, int32_t b, int32_t fix, int32_t result) { - Rand::LockGuard fg(fix); + auto scoped = Rand::test::makeScopedRNGExchange(fix); for (int i = 0; i < 10; ++i) { auto x = Rand::GetRandomNumber(a, b); REQUIRE_EQ(x, result); @@ -96,8 +89,8 @@ TEST_CASE("GetRandomNumberFixed") { testGetRandomNumberFixed(-10, 12, 5, 5); testGetRandomNumberFixed(-10, 12, 12, 12); testGetRandomNumberFixed(-10, 12, 55, 12); - testGetRandomNumberFixed(-10, 12, INT32_MIN, -10); - testGetRandomNumberFixed(-10, 12, INT32_MAX, 12); + testGetRandomNumberFixed(-10, 12, std::numeric_limits::min(), -10); + testGetRandomNumberFixed(-10, 12, std::numeric_limits::max(), 12); } TEST_SUITE_END();