/** * @file random.hpp * * Contains convenience functions for random number generation * * This includes specific engine/distribution functions for logic that needs to be compatible with the base game. */ #pragma once #include #include #include #include namespace devilution { class DiabloGenerator { private: /** Borland C/C++ psuedo-random number generator needed for vanilla compatibility */ std::linear_congruential_engine lcg; public: /** * @brief Set the state of the RandomNumberEngine used by the base game to the specific seed * @param seed New engine state */ DiabloGenerator(uint32_t seed) { lcg.seed(seed); } /** * @brief Advance the global RandomNumberEngine state by the specified number of rounds * * Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated. * @param count How many values to discard */ void discardRandomValues(unsigned count) { lcg.discard(count); } /** * @brief Generates a random non-negative integer (most of the time) using the vanilla RNG * * This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try * discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31. * * This function is only used when the base game wants to store the seed used to generate an item or level, however * as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more * appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game. * * @return A random number in the range [0,2^31) or -2^31 */ int32_t advanceRndSeed() { const int32_t seed = static_cast(lcg()); // since abs(INT_MIN) is undefined behavior, handle this value specially return seed == std::numeric_limits::min() ? std::numeric_limits::min() : std::abs(seed); } /** * @brief Generates a random integer less than the given limit using the vanilla RNG * * If v is not a positive number this function returns 0 without calling the RNG. * * Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value * less than 32768 for limits in that range. * * This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed() * * @see AdvanceRndSeed() * @param v The upper limit for the return value * @return A random number in the range [0, v) or rarely a negative value in (-v, -1] */ int32_t generateRnd(int32_t v) { if (v <= 0) return 0; if (v <= 0x7FFF) // use the high bits to correct for LCG bias return (advanceRndSeed() >> 16) % v; return advanceRndSeed() % v; } /** * @brief Generates a random boolean value using the vanilla RNG * * This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2 * represents a 50/50 chance. * * @param frequency odds of returning a true value * @return A random boolean value */ bool flipCoin(unsigned frequency) { // Casting here because GenerateRnd takes a signed argument when it should take and yield unsigned. return generateRnd(static_cast(frequency)) == 0; } /** * @brief Picks one of the elements in the list randomly. * * @param values The values to pick from * @return A random value from the 'values' list. */ template const T pickRandomlyAmong(const std::initializer_list &values) { const auto index { std::max(generateRnd(static_cast(values.size())), 0) }; return *(values.begin() + index); } /** * @brief Generates a random non-negative integer * * Effectively the same as GenerateRnd but will never return a negative value * @param v upper limit for the return value * @return a value between 0 and v-1 inclusive, i.e. the range [0, v) */ inline int32_t randomIntLessThan(int32_t v) { return std::max(generateRnd(v), 0); } /** * @brief Randomly chooses a value somewhere within the given range * @param min lower limit, minimum possible value * @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range * @param halfOpen whether to use the limits as a half-open range or not * @return a randomly selected integer */ inline int32_t randomIntBetween(int32_t min, int32_t max, bool halfOpen = false) { return randomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min; } }; // Based on fmix32 implementation from MurmurHash3 created by Austin Appleby in 2008 // https://github.com/aappleby/smhasher/blob/61a0530f28277f2e850bfc39600ce61d02b518de/src/MurmurHash3.cpp#L68 // and adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna // // See also: // Guy L. Steele, Doug Lea, and Christine H. Flood. 2014. // Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472. // https://doi.org/10.1145/2714064.2660195 class SplitMix32 { uint32_t state; public: SplitMix32(uint32_t state) : state(state) { } uint32_t next() { uint32_t z = (state += 0x9e3779b9); z = (z ^ (z >> 16)) * 0x85ebca6b; z = (z ^ (z >> 13)) * 0xc2b2ae35; return z ^ (z >> 16); } void generate(uint32_t *begin, const uint32_t *end) { while (begin != end) { *begin = next(); ++begin; } } }; // Adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna // // See also: // Guy L. Steele, Doug Lea, and Christine H. Flood. 2014. // Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472. // https://doi.org/10.1145/2714064.2660195 class SplitMix64 { uint64_t state; public: SplitMix64(uint64_t state) : state(state) { } uint64_t next() { uint64_t z = (state += 0x9e3779b97f4a7c15); z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; z = (z ^ (z >> 27)) * 0x94d049bb133111eb; return z ^ (z >> 31); } void generate(uint64_t *begin, const uint64_t *end) { while (begin != end) { *begin = next(); ++begin; } } }; /** Adapted from https://prng.di.unimi.it/xoshiro128plusplus.c written in 2019 by David Blackman and Sebastiano Vigna */ class xoshiro128plusplus { public: typedef uint32_t state[4]; xoshiro128plusplus() { seed(); } xoshiro128plusplus(const state &s) { copy(this->s, s); } xoshiro128plusplus(uint64_t initialSeed) { seed(initialSeed); } xoshiro128plusplus(uint32_t initialSeed) { seed(initialSeed); } uint32_t next(); /* This is the jump function for the generator. It is equivalent to 2^64 calls to next(); it can be used to generate 2^64 non-overlapping subsequences for parallel computations. */ void jump() { static constexpr uint32_t JUMP[] = { 0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b }; uint32_t s0 = 0; uint32_t s1 = 0; uint32_t s2 = 0; uint32_t s3 = 0; for (const uint32_t entry : JUMP) for (int b = 0; b < 32; b++) { if (entry & UINT32_C(1) << b) { s0 ^= s[0]; s1 ^= s[1]; s2 ^= s[2]; s3 ^= s[3]; } next(); } s[0] = s0; s[1] = s1; s[2] = s2; s[3] = s3; } void save(state &s) const { copy(s, this->s); } private: state s; void seed(uint64_t value) { uint64_t seeds[2]; SplitMix64 seedSequence { value }; seedSequence.generate(seeds, seeds + 2); s[0] = static_cast(seeds[0] >> 32); s[1] = static_cast(seeds[0]); s[2] = static_cast(seeds[1] >> 32); s[3] = static_cast(seeds[1]); } void seed(uint32_t value) { SplitMix32 seedSequence { value }; seedSequence.generate(s, s + 4); } void seed() { seed(timeSeed()); #if !(defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0)) static std::random_device rd; std::uniform_int_distribution dist; for (uint32_t &cell : s) cell ^= dist(rd); #endif } static uint64_t timeSeed(); static void copy(state &dst, const state &src); }; /** * @brief Returns a copy of the global seed generator and fast-forwards the global seed generator to avoid collisions */ xoshiro128plusplus ReserveSeedSequence(); /** * @brief Advances the global seed generator state and returns the new value */ uint32_t GenerateSeed(); /** * @brief Set the state of the RandomNumberEngine used by the base game to the specific seed * @param seed New engine state */ void SetRndSeed(uint32_t seed); /** * @brief Returns the current state of the RandomNumberEngine used by the base game * * This is only exposed to allow for debugging vanilla code and testing. Using this engine for new code is discouraged * due to the poor randomness and bugs in the implementation that need to be retained for compatibility. * * @return The current engine state */ uint32_t GetLCGEngineState(); /** * @brief Advance the global RandomNumberEngine state by the specified number of rounds * * Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated. * @param count How many values to discard */ void DiscardRandomValues(unsigned count); /** * @brief Advances the global RandomNumberEngine state and returns the new value */ uint32_t GenerateRandomNumber(); /** * @brief Generates a random non-negative integer (most of the time) using the vanilla RNG * * This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try * discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31. * * This function is only used when the base game wants to store the seed used to generate an item or level, however * as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more * appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game. * * @return A random number in the range [0,2^31) or -2^31 */ [[nodiscard]] int32_t AdvanceRndSeed(); /** * @brief Generates a random integer less than the given limit using the vanilla RNG * * If v is not a positive number this function returns 0 without calling the RNG. * * Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value * less than 32768 for limits in that range. * * This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed() * * @see AdvanceRndSeed() * @param v The upper limit for the return value * @return A random number in the range [0, v) or rarely a negative value in (-v, -1] */ int32_t GenerateRnd(int32_t v); /** * @brief Generates a random boolean value using the vanilla RNG * * This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2 * represents a 50/50 chance. * * @param frequency odds of returning a true value * @return A random boolean value */ bool FlipCoin(unsigned frequency = 2); /** * @brief Picks one of the elements in the list randomly. * * @param values The values to pick from * @return A random value from the 'values' list. */ template const T PickRandomlyAmong(const std::initializer_list &values) { const auto index { std::max(GenerateRnd(static_cast(values.size())), 0) }; return *(values.begin() + index); } /** * @brief Generates a random non-negative integer * * Effectively the same as GenerateRnd but will never return a negative value * @param v upper limit for the return value * @return a value between 0 and v-1 inclusive, i.e. the range [0, v) */ inline int32_t RandomIntLessThan(int32_t v) { return std::max(GenerateRnd(v), 0); } /** * @brief Randomly chooses a value somewhere within the given range * @param min lower limit, minimum possible value * @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range * @param halfOpen whether to use the limits as a half-open range or not * @return a randomly selected integer */ inline int32_t RandomIntBetween(int32_t min, int32_t max, bool halfOpen = false) { return RandomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min; } } // namespace devilution