// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Random { using System; using System.Runtime.Serialization; using System.Text.Json.Serialization; using Helper; using ProtoBuf; /// /// A high-quality, small-state pseudo-random number generator based on the PCG family. /// /// /// /// Implementation based off of the reference PCG Random, found here: https://www.pcg-random.org/index.html /// /// /// PCG (Permuted Congruential Generator) offers excellent statistical quality with very small state /// and extremely fast generation. This implementation uses a 64-bit state with 32-bit outputs and /// an increment (stream selector) to avoid overlapping sequences when constructing multiple instances. /// /// /// Pros: /// /// /// /// Fast and allocation-free; suitable for gameplay hot paths. /// /// /// Great statistical quality for games and simulations; passes common PRNG test suites for 32-bit outputs. /// /// /// Deterministic and reproducible across platforms for identical seeds. /// /// /// Small state footprint; trivial to serialize via . /// /// /// /// Cons: /// /// /// /// Not cryptographically secure; do not use for security-sensitive tokens or secrets. /// /// /// 32-bit outputs; if you need full 64-bit outputs, consider generating two uint values or using a 64-bit variant. /// /// /// /// When to use: /// /// /// /// General gameplay randomness, procedural content, Monte Carlo style sampling. /// /// /// Situations requiring deterministic replays by capturing and restoring . /// /// /// /// When not to use: /// /// /// /// Cryptographic or adversarial scenarios. /// /// /// When you specifically need UnityEngine.Random’s global state behavior; use for parity. /// /// /// /// Threading: prefer accessing via ThreadLocalRandom<PcgRandom>.Instance or (which returns the default PRNG) /// to avoid contention and accidental shared-state between threads. /// /// /// /// /// using WallstopStudios.UnityHelpers.Core.Random; /// /// // Recommended: use the global default PRNG (thread-local instance) /// IRandom rng = PRNG.Instance; // currently IllusionFlow; swap to PcgRandom easily /// int value = rng.Next(0, 100); /// float probability = rng.NextFloat(); /// bool coinFlip = rng.NextBool(); /// /// // Deterministic playthrough: capture and restore state /// var seeded = new PcgRandom(seed: 123456789L); /// RandomState snapshot = seeded.InternalState; /// // ... generate values /// var replay = new PcgRandom(snapshot); /// // replay now yields identical sequence /// /// // Weighted selection (via extensions): /// // var index = rng.NextWeightedIndex(new float[] { 0.1f, 0.3f, 0.6f }); /// /// [RandomGeneratorMetadata( RandomQuality.Excellent, "PCG XSH RR 64/32 variant; passes TestU01 BigCrush and PractRand in published results.", "O'Neill 2014", "https://www.pcg-random.org/paper.html" )] [Serializable] [DataContract] [ProtoContract] public sealed class PcgRandom : AbstractRandom, IEquatable, IComparable, IComparable { private static ulong NormalizeIncrement(ulong increment) { return (increment & 1UL) == 0 ? increment | 1UL : increment; } public static PcgRandom Instance => ThreadLocalRandom.Instance; public override RandomState InternalState => BuildState(_state, _increment); [ProtoMember(6)] internal readonly ulong _increment; [ProtoMember(7)] internal ulong _state; public PcgRandom() : this(Guid.NewGuid()) { } public PcgRandom(Guid guid) { (ulong a, ulong b) = RandomUtilities.GuidToUInt64Pair(guid); _state = a; _increment = NormalizeIncrement(b); } [JsonConstructor] public PcgRandom(RandomState internalState) { _state = internalState.State1; _increment = NormalizeIncrement(internalState.State2); RestoreCommonState(internalState); } public PcgRandom(ulong increment, ulong state) { _increment = NormalizeIncrement(increment); _state = state; } public PcgRandom(long seed) { // Start with a nice prime _increment = NormalizeIncrement(6554638469UL); _state = unchecked((ulong)seed); _increment = NormalizeIncrement(NextUlong()); } public override uint NextUint() { unchecked { ulong oldState = _state; _state = oldState * 6364136223846793005UL + _increment; uint xorShifted = (uint)(((oldState >> 18) ^ oldState) >> 27); int rot = (int)(oldState >> 59); return (xorShifted >> rot) | (xorShifted << (-rot & 31)); } } public bool Equals(PcgRandom other) { if (ReferenceEquals(other, null)) { return false; } // ReSharper disable once CompareOfFloatsByEqualityOperator return _increment == other._increment && _state == other._state && _cachedGaussian == other._cachedGaussian; } public override bool Equals(object obj) { return Equals(obj as PcgRandom); } public int CompareTo(PcgRandom other) { if (ReferenceEquals(other, null)) { return -1; } if (_increment == other._increment) { if (_state == other._state) { return 0; } if (_state < other._state) { return -1; } return 1; } if (_increment < other._increment) { return -1; } if (_cachedGaussian.HasValue != other._cachedGaussian.HasValue) { return _cachedGaussian.HasValue ? -1 : 1; } if (!_cachedGaussian.HasValue) { return 0; } // ReSharper disable once PossibleInvalidOperationException return _cachedGaussian.Value.CompareTo(other._cachedGaussian.Value); } public int CompareTo(object obj) { return CompareTo(obj as PcgRandom); } public override int GetHashCode() { return Objects.HashCode(_increment, _state, _cachedGaussian); } public override string ToString() { return $"{{\"Increment\": {_increment}, \"State\": {_state}}}"; } public override IRandom Copy() { return new PcgRandom(InternalState); } } }