diff --git a/README.md b/README.md
index 4368cdc..01015de 100644
--- a/README.md
+++ b/README.md
@@ -106,6 +106,34 @@ Fixed64 sinValue = FixedTrigonometry.Sin(angle);
Console.WriteLine(sinValue); // Output: ~0.707
```
+### Deterministic Random Generation
+
+Use `DeterministicRandom` when you need reproducible random values across runs, worlds, or features.
+Streams are derived from a seed and remain deterministic regardless of threading or platform.
+
+```csharp
+// Simple constructor-based stream:
+var rng = new DeterministicRandom(42UL);
+
+// Deterministic integer:
+int value = rng.Next(1, 10); // [1,10)
+
+// Deterministic Fixed64 in [0,1):
+Fixed64 ratio = rng.NextFixed6401();
+
+// One stream per “feature” that’s stable for the same worldSeed + key:
+var rngOre = DeterministicRandom.FromWorldFeature(worldSeed: 123456789UL, featureKey: 0xORE);
+var rngRivers = DeterministicRandom.FromWorldFeature(123456789UL, 0xRIV, index: 0);
+
+// Deterministic Fixed64 draws:
+Fixed64 h = rngOre.NextFixed64(Fixed64.One); // [0, 1)
+Fixed64 size = rngOre.NextFixed64(Fixed64.Zero, 5 * Fixed64.One); // [0, 5)
+Fixed64 posX = rngRivers.NextFixed64(-Fixed64.One, Fixed64.One); // [-1, 1)
+
+// Deterministic integers:
+int loot = rngOre.Next(1, 5); // [1,5)
+```
+
---
## 📦 Library Structure
@@ -116,6 +144,7 @@ Console.WriteLine(sinValue); // Output: ~0.707
- **`IBound` Interface:** Standard interface for bounding shapes `BoundingBox`, `BoundingArea`, and `BoundingSphere`, each offering intersection, containment, and projection logic.
- **`FixedMath` Static Class:** Provides common math and trigonometric functions using fixed-point math.
- **`Fixed4x4` and `Fixed3x3`:** Support matrix operations for transformations.
+- **`DeterministicRandom` Struct:** Seedable, allocation-free RNG for repeatable procedural generation.
### Fixed64 Struct
diff --git a/src/FixedMathSharp/Utility/DeterministicRandom.cs b/src/FixedMathSharp/Utility/DeterministicRandom.cs
new file mode 100644
index 0000000..51e216b
--- /dev/null
+++ b/src/FixedMathSharp/Utility/DeterministicRandom.cs
@@ -0,0 +1,211 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace FixedMathSharp.Utility
+{
+ ///
+ /// Fast, seedable, deterministic RNG suitable for lockstep sims and map gen.
+ /// Uses xoroshiro128++ with splitmix64 seeding. No allocations, no time/GUID.
+ ///
+ public struct DeterministicRandom
+ {
+ // xoroshiro128++ state
+ private ulong _s0;
+ private ulong _s1;
+
+ #region Construction / Seeding
+
+ public DeterministicRandom(ulong seed)
+ {
+ // Expand a single seed into two 64-bit state words via splitmix64.
+ _s0 = SplitMix64(ref seed);
+ _s1 = SplitMix64(ref seed);
+
+ // xoroshiro requires non-zero state; repair pathological seed.
+ if (_s0 == 0UL && _s1 == 0UL)
+ _s1 = 0x9E3779B97F4A7C15UL;
+ }
+
+ ///
+ /// Create a stream deterministically
+ /// Derived from (worldSeed, featureKey[,index]).
+ ///
+ public static DeterministicRandom FromWorldFeature(ulong worldSeed, ulong featureKey, ulong index = 0)
+ {
+ // Simple reversible mix (swap for a stronger mix if required).
+ ulong seed = Mix64(worldSeed, featureKey);
+ seed = Mix64(seed, index);
+ return new DeterministicRandom(seed);
+ }
+
+ #endregion
+
+ #region Core PRNG
+
+ ///
+ /// xoroshiro128++ next 64 bits.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ulong NextU64()
+ {
+ ulong s0 = _s0, s1 = _s1;
+ ulong result = RotL(s0 + s1, 17) + s0;
+
+ s1 ^= s0;
+ _s0 = RotL(s0, 49) ^ s1 ^ (s1 << 21); // a,b
+ _s1 = RotL(s1, 28); // c
+
+ return result;
+ }
+
+ ///
+ /// Next non-negative Int32 in [0, int.MaxValue].
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int Next()
+ {
+ // Take high bits for better quality; mask to 31 bits non-negative.
+ return (int)(NextU64() >> 33);
+ }
+
+ ///
+ /// Unbiased int in [0, maxExclusive).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int Next(int maxExclusive)
+ {
+ return maxExclusive <= 0
+ ? throw new ArgumentOutOfRangeException(nameof(maxExclusive))
+ : (int)NextBounded((uint)maxExclusive);
+ }
+
+ ///
+ /// Unbiased int in [min, maxExclusive).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int Next(int minInclusive, int maxExclusive)
+ {
+ if (minInclusive >= maxExclusive)
+ throw new ArgumentException("min >= max");
+ uint range = (uint)(maxExclusive - minInclusive);
+ return minInclusive + (int)NextBounded(range);
+ }
+
+ ///
+ /// Double in [0,1).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public double NextDouble()
+ {
+ // 53 random bits -> [0,1)
+ return (NextU64() >> 11) * (1.0 / (1UL << 53));
+ }
+
+ ///
+ /// Fill span with random bytes.
+ ///
+ public void NextBytes(Span buffer)
+ {
+ int i = 0;
+ while (i + 8 <= buffer.Length)
+ {
+ ulong v = NextU64();
+ Unsafe.WriteUnaligned(ref buffer[i], v);
+ i += 8;
+ }
+ if (i < buffer.Length)
+ {
+ ulong v = NextU64();
+ while (i < buffer.Length)
+ {
+ buffer[i++] = (byte)v;
+ v >>= 8;
+ }
+ }
+ }
+
+ #endregion
+
+ #region Fixed64 helpers
+
+ ///
+ /// Random Fixed64 in [0,1).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Fixed64 NextFixed6401()
+ {
+ // Produce a raw value in [0, One.m_rawValue)
+ ulong rawOne = (ulong)Fixed64.One.m_rawValue;
+ ulong r = NextBounded(rawOne);
+ return Fixed64.FromRaw((long)r);
+ }
+
+ ///
+ /// Random Fixed64 in [0, maxExclusive).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Fixed64 NextFixed64(Fixed64 maxExclusive)
+ {
+ if (maxExclusive <= Fixed64.Zero)
+ throw new ArgumentOutOfRangeException(nameof(maxExclusive), "max must be > 0");
+ ulong rawMax = (ulong)maxExclusive.m_rawValue;
+ ulong r = NextBounded(rawMax);
+ return Fixed64.FromRaw((long)r);
+ }
+
+ ///
+ /// Random Fixed64 in [minInclusive, maxExclusive).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Fixed64 NextFixed64(Fixed64 minInclusive, Fixed64 maxExclusive)
+ {
+ if (minInclusive >= maxExclusive)
+ throw new ArgumentException("min >= max");
+ ulong span = (ulong)(maxExclusive.m_rawValue - minInclusive.m_rawValue);
+ ulong r = NextBounded(span);
+ return Fixed64.FromRaw((long)r + minInclusive.m_rawValue);
+ }
+
+ #endregion
+
+ #region Internals: unbiased range, splitmix64, mixing, rotations
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private ulong NextBounded(ulong bound)
+ {
+ // Rejection to avoid modulo bias.
+ // threshold = 2^64 % bound, but expressed as (-bound) % bound
+ ulong threshold = unchecked((ulong)-(long)bound) % bound;
+ while (true)
+ {
+ ulong r = NextU64();
+ if (r >= threshold)
+ return r % bound;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong RotL(ulong x, int k) => (x << k) | (x >> (64 - k));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong SplitMix64(ref ulong state)
+ {
+ ulong z = (state += 0x9E3779B97F4A7C15UL);
+ z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL;
+ z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL;
+ return z ^ (z >> 31);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static ulong Mix64(ulong a, ulong b)
+ {
+ // Simple reversible mix (variant of splitmix finalizer).
+ ulong x = a ^ (b + 0x9E3779B97F4A7C15UL);
+ x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9UL;
+ x = (x ^ (x >> 27)) * 0x94D049BB133111EBUL;
+ return x ^ (x >> 31);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/FixedMathSharp/Utility/ThreadLocalRandom.cs b/src/FixedMathSharp/Utility/ThreadLocalRandom.cs
index 23680a2..393da6c 100644
--- a/src/FixedMathSharp/Utility/ThreadLocalRandom.cs
+++ b/src/FixedMathSharp/Utility/ThreadLocalRandom.cs
@@ -4,113 +4,68 @@
namespace FixedMathSharp.Utility
{
///
- /// Provides thread-safe, deterministic random number generation for use in simulations, games,
- /// and physics engines. This utility ensures randomness is consistent across multiple threads,
- /// avoiding common pitfalls of shared Random instances and aiding reproducible calculations.
+ /// Deterministic per-thread RNG facade.
///
+ [Obsolete("ThreadLocalRandom is deprecated. Use DeterministicRandom or DeterministicRandom.FromWorldFeature(...) for deterministic streams.", false)]
public static class ThreadLocalRandom
{
- ///
- /// Random number generator used to generate seeds,
- /// which are then used to create new random number
- /// generators on a per-thread basis.
- ///
- private static readonly Random globalRandom = new Random(Guid.NewGuid().GetHashCode());
- private static readonly object globalLock = new object();
-
- ///
- /// Random number generator
- ///
- private static readonly ThreadLocal threadRandom = new ThreadLocal(NewRandom);
+ private static ulong _rootSeed = 0;
+ private static Func _threadIndexProvider = null!;
+ private static ThreadLocal _threadRng = null!;
///
- /// Creates a new instance of Random. The seed is derived
- /// from a global (static) instance of Random, rather
- /// than time.
+ /// Initialize global deterministic seeding.
+ /// Provide a stable threadIndex (0..T-1) for each thread.
///
- public static Random NewRandom()
+ public static void Initialize(ulong rootSeed, Func threadIndexProvider)
{
- lock (globalLock)
- return new Random(globalRandom.Next());
+ _rootSeed = rootSeed;
+ _threadIndexProvider = threadIndexProvider ?? throw new ArgumentNullException(nameof(threadIndexProvider));
+
+ _threadRng = new ThreadLocal(() =>
+ {
+ int idx = _threadIndexProvider();
+ // Derive a unique stream per thread deterministically from rootSeed + idx.
+ return DeterministicRandom.FromWorldFeature(_rootSeed, (ulong)idx);
+ });
}
///
- /// Returns an instance of Random which can be used freely
- /// within the current thread.
+ /// Create a new independent RNG from a specific seed (does not affect thread Instance).
///
- public static Random Instance => threadRandom.Value;
-
- /// See
- public static int Next()
- {
- return Instance.Next();
- }
-
- /// See
- public static int Next(int maxValue)
- {
- return Instance.Next(maxValue);
- }
-
- /// See
- public static int Next(int minValue, int maxValue)
- {
- return Instance.Next(minValue, maxValue);
- }
+ public static DeterministicRandom NewRandom(ulong seed) => new(seed);
///
- /// Returns a random Fixed64 number that is less than `max`.
+ /// Per-thread RNG instance (requires Initialize to be called first).
///
- ///
- public static Fixed64 NextFixed64(Fixed64 max = default)
+ public static DeterministicRandom Instance
{
- if (max == Fixed64.Zero)
- throw new ArgumentException("Max value must be greater than zero.");
-
- byte[] buf = new byte[8];
- Instance.NextBytes(buf);
-
- // Use bitwise operation to ensure a non-negative long.
- long longRand = BitConverter.ToInt64(buf, 0) & long.MaxValue;
-
- return Fixed64.FromRaw(longRand % max.m_rawValue);
+ get
+ {
+ if (_threadRng == null)
+ throw new InvalidOperationException("ThreadLocalRandom.Initialize(rootSeed, threadIndexProvider) must be called first.");
+ return _threadRng.Value;
+ }
}
- ///
- /// Returns a random Fixed64 number that is greater than or equal to `min`, and less than `max`.
- ///
- ///
- ///
- public static Fixed64 NextFixed64(Fixed64 min, Fixed64 max)
- {
- if (min >= max)
- throw new ArgumentException("Min value must be less than max.");
-
- byte[] buf = new byte[8];
- Instance.NextBytes(buf);
-
- // Ensure non-negative random long.
- long longRand = BitConverter.ToInt64(buf, 0) & long.MaxValue;
+ #region Convenience mirrors
- return Fixed64.FromRaw(longRand % (max.m_rawValue - min.m_rawValue)) + min;
- }
+ public static int Next() => Instance.Next();
+ public static int Next(int maxExclusive) => Instance.Next(maxExclusive);
+ public static int Next(int minInclusive, int maxExclusive) => Instance.Next(minInclusive, maxExclusive);
+ public static double NextDouble() => Instance.NextDouble();
+ public static double NextDouble(double min, double max) => Instance.NextDouble() * (max - min) + min;
- /// See
- public static double NextDouble()
- {
- return Instance.NextDouble();
- }
-
- /// See
- public static double NextDouble(double min, double max)
- {
- return Instance.NextDouble() * (max - min) + min;
- }
-
- /// See
public static void NextBytes(byte[] buffer)
{
+ if (buffer == null) throw new ArgumentNullException(nameof(buffer));
Instance.NextBytes(buffer);
}
+
+ public static Fixed64 NextFixed6401() => Instance.NextFixed6401();
+ public static Fixed64 NextFixed64(Fixed64 maxExclusive) => Instance.NextFixed64(maxExclusive);
+ public static Fixed64 NextFixed64(Fixed64 minInclusive, Fixed64 maxExclusive) => Instance.NextFixed64(minInclusive, maxExclusive);
+
+ #endregion
}
-}
\ No newline at end of file
+}
diff --git a/tests/FixedMathSharp.Tests/DeterministicRandom.Tests.cs b/tests/FixedMathSharp.Tests/DeterministicRandom.Tests.cs
new file mode 100644
index 0000000..95d72ab
--- /dev/null
+++ b/tests/FixedMathSharp.Tests/DeterministicRandom.Tests.cs
@@ -0,0 +1,297 @@
+using System;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using FixedMathSharp.Utility;
+using Xunit;
+
+namespace FixedMathSharp.Tests
+{
+ public class DeterministicRandomTests
+ {
+ // Helper: pull a sequence from NextU64 to compare streams
+ private static ulong[] U64Seq(DeterministicRandom rng, int count)
+ {
+ var arr = new ulong[count];
+ for (int i = 0; i < count; i++) arr[i] = rng.NextU64();
+ return arr;
+ }
+
+ // Helper: pull a sequence from Next(int) to compare streams
+ private static int[] IntSeq(DeterministicRandom rng, int count, int min, int max)
+ {
+ var arr = new int[count];
+ for (int i = 0; i < count; i++) arr[i] = rng.Next(min, max);
+ return arr;
+ }
+
+ [Fact]
+ public void SameSeed_Yields_IdenticalSequences()
+ {
+ var a = new DeterministicRandom(123456789UL);
+ var b = new DeterministicRandom(123456789UL);
+
+ // Interleave various calls to ensure internal state advances identically
+ Assert.Equal(a.NextU64(), b.NextU64());
+ Assert.Equal(a.Next(), b.Next());
+ Assert.Equal(a.Next(1000), b.Next(1000));
+ Assert.Equal(a.Next(-50, 50), b.Next(-50, 50));
+ Assert.Equal(a.NextDouble(), b.NextDouble(), 14);
+
+ // Then compare a longer run of NextU64 to be extra sure
+ var seqA = U64Seq(a, 32);
+ var seqB = U64Seq(b, 32);
+ Assert.Equal(seqA, seqB);
+ }
+
+ [Fact]
+ public void DifferentSeeds_Yield_DifferentSequences()
+ {
+ var a = new DeterministicRandom(1UL);
+ var b = new DeterministicRandom(2UL);
+
+ // It's possible (but astronomically unlikely) that first value matches; check a window
+ var seqA = U64Seq(a, 16);
+ var seqB = U64Seq(b, 16);
+
+ // Require at least one difference in the first 16 draws
+ Assert.NotEqual(seqA, seqB);
+ }
+
+ [Fact]
+ public void FromWorldFeature_IsStable_AndSeparatesByFeature_AndIndex()
+ {
+ ulong world = 0xDEADBEEFCAFEBABEUL;
+ ulong featureOre = 0x4F5245UL; // 'ORE'
+ ulong featureRiver = 0x524956UL; // 'RIV'
+
+ // Stability: repeated construction yields same sequence
+ var a1 = DeterministicRandom.FromWorldFeature(world, featureOre, index: 0);
+ var a2 = DeterministicRandom.FromWorldFeature(world, featureOre, index: 0);
+ Assert.Equal(U64Seq(a1, 8), U64Seq(a2, 8));
+
+ // Different featureKey -> different stream
+ var b = DeterministicRandom.FromWorldFeature(world, featureRiver, index: 0);
+ Assert.NotEqual(U64Seq(a1, 8), U64Seq(b, 8));
+
+ // Different index -> different stream
+ var c = DeterministicRandom.FromWorldFeature(world, featureOre, index: 1);
+ Assert.NotEqual(U64Seq(a1, 8), U64Seq(c, 8));
+ }
+
+ [Fact]
+ public void Next_NoArg_IsNonNegative_AndWithinRange()
+ {
+ var rng = new DeterministicRandom(42UL);
+ for (int i = 0; i < 1000; i++)
+ {
+ int v = rng.Next();
+ Assert.InRange(v, 0, int.MaxValue);
+ }
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(7)]
+ [InlineData(97)]
+ [InlineData(int.MaxValue)]
+ public void Next_MaxExclusive_RespectsBounds_AndThrows_OnInvalid(int maxExclusive)
+ {
+ var rng = new DeterministicRandom(9001UL);
+ for (int i = 0; i < 4096; i++)
+ {
+ int v = rng.Next(maxExclusive);
+ Assert.InRange(v, 0, maxExclusive - 1);
+ }
+ }
+
+ [Fact]
+ public void Next_MaxExclusive_Throws_WhenNonPositive()
+ {
+ var rng = new DeterministicRandom(1UL);
+ Assert.Throws(() => rng.Next(0));
+ Assert.Throws(() => rng.Next(-5));
+ }
+
+ [Fact]
+ public void Next_MinMax_RespectsBounds_AndThrows_OnInvalidRange()
+ {
+ var rng = new DeterministicRandom(2024UL);
+
+ for (int i = 0; i < 4096; i++)
+ {
+ int v = rng.Next(-10, 10);
+ Assert.InRange(v, -10, 9);
+ }
+
+ Assert.Throws(() => rng.Next(5, 5));
+ Assert.Throws(() => rng.Next(10, -10));
+ Assert.Throws(() => rng.Next(10, 10));
+ }
+
+ [Fact]
+ public void NextDouble_IsInUnitInterval()
+ {
+ var rng = new DeterministicRandom(123UL);
+ for (int i = 0; i < 4096; i++)
+ {
+ double d = rng.NextDouble();
+ Assert.True(d >= 0.0 && d < 1.0, "NextDouble() must be in [0,1)");
+ }
+ }
+
+ [Fact]
+ public void NextBytes_FillsEntireBuffer_AndAdvancesState()
+ {
+ var rng = new DeterministicRandom(55555UL);
+
+ // Exercise both the fast 8-byte path and the tail path
+ var sizes = new[] { 1, 7, 8, 9, 10, 15, 16, 17, 31, 32, 33 };
+ byte[][] results = new byte[sizes.Length][];
+
+ for (int i = 0; i < sizes.Length; i++)
+ {
+ var buf = new byte[sizes[i]];
+ rng.NextBytes(buf);
+ // Ensure something was written (not all zeros) and length correct
+ Assert.Equal(sizes[i], buf.Length);
+ Assert.True(buf.Any(b => b != 0) || sizes[i] == 0);
+ results[i] = buf;
+ }
+
+ // Make sure successive calls produce different buffers (very high probability)
+ for (int i = 1; i < sizes.Length; i++)
+ {
+ Assert.NotEqual(results[i - 1], results[i]);
+ }
+ }
+
+ [Fact]
+ public void Fixed64_ZeroToOne_IsWithinRange_AndReproducible()
+ {
+ var rng1 = new DeterministicRandom(777UL);
+ var rng2 = new DeterministicRandom(777UL);
+
+ for (int i = 0; i < 2048; i++)
+ {
+ var a = rng1.NextFixed6401(); // [0,1)
+ var b = rng2.NextFixed6401();
+ Assert.True(a >= Fixed64.Zero && a < Fixed64.One);
+ Assert.Equal(a, b); // same seed, same draw order → identical
+ }
+ }
+
+ [Fact]
+ public void Fixed64_MaxExclusive_IsWithinRange_AndThrowsOnInvalid()
+ {
+ var rng = new DeterministicRandom(888UL);
+
+ // Positive max
+ var max = 5 * Fixed64.One;
+ for (int i = 0; i < 2048; i++)
+ {
+ var v = rng.NextFixed64(max);
+ Assert.True(v >= Fixed64.Zero && v < max);
+ }
+
+ // Invalid: max <= 0
+ Assert.Throws(() => rng.NextFixed64(Fixed64.Zero));
+ Assert.Throws(() => rng.NextFixed64(-Fixed64.One));
+ }
+
+ [Fact]
+ public void Fixed64_MinMax_RespectsBounds_AndThrowsOnInvalidRange()
+ {
+ var rng = new DeterministicRandom(999UL);
+
+ var min = -Fixed64.One;
+ var max = 2 * Fixed64.One;
+
+ for (int i = 0; i < 2048; i++)
+ {
+ var v = rng.NextFixed64(min, max);
+ Assert.True(v >= min && v < max);
+ }
+
+ Assert.Throws(() => rng.NextFixed64(Fixed64.One, Fixed64.One));
+ Assert.Throws(() => rng.NextFixed64(Fixed64.One, Fixed64.Zero));
+ }
+
+ [Fact]
+ public void Interleaved_APIs_Stay_Deterministic()
+ {
+ // Ensure mixing different API calls doesn't desynchronize deterministic equality
+ var a1 = new DeterministicRandom(1234UL);
+ var a2 = new DeterministicRandom(1234UL);
+
+ // Apply the same interleaving sequence on both instances
+ int i1 = a1.Next(100);
+ int i2 = a2.Next(100);
+ Assert.Equal(i1, i2);
+
+ double d1 = a1.NextDouble();
+ double d2 = a2.NextDouble();
+ Assert.Equal(d1, d2, 14);
+
+ var buf1 = new byte[13];
+ var buf2 = new byte[13];
+ a1.NextBytes(buf1);
+ a2.NextBytes(buf2);
+ Assert.Equal(buf1, buf2);
+
+ var f1 = a1.NextFixed64(-Fixed64.One, Fixed64.One);
+ var f2 = a2.NextFixed64(-Fixed64.One, Fixed64.One);
+ Assert.Equal(f1, f2);
+
+ // And the streams continue to match:
+ Assert.Equal(a1.NextU64(), a2.NextU64());
+ Assert.Equal(a1.Next(), a2.Next());
+ }
+
+ [Fact]
+ public void Next_Int_Bounds_Cover_CommonAndEdgeRanges()
+ {
+ var rng = new DeterministicRandom(3141592653UL);
+
+ // Small bounds including 1 (degenerate but valid)
+ foreach (int bound in new[] { 1, 2, 3, 4, 5, 7, 16, 31, 32, 33, 64, 127, 128, 129, 255, 256, 257 })
+ {
+ for (int i = 0; i < 512; i++)
+ {
+ int v = rng.Next(bound);
+ Assert.InRange(v, 0, bound - 1);
+ }
+ }
+
+ // Wide range near int.MaxValue to exercise rejection logic frequently
+ for (int i = 0; i < 1024; i++)
+ {
+ int v = rng.Next(int.MaxValue);
+ Assert.InRange(v, 0, int.MaxValue - 1);
+ }
+ }
+
+ [Fact]
+ public void Next_Fixed64_Covers_TypicalGameRanges()
+ {
+ var rng = new DeterministicRandom(0xFEEDFACEUL);
+
+ // [0, 10)
+ var ten = 10 * Fixed64.One;
+ for (int i = 0; i < 2048; i++)
+ {
+ var v = rng.NextFixed64(ten);
+ Assert.True(v >= Fixed64.Zero && v < ten);
+ }
+
+ // [-5, 5)
+ var neg5 = -5 * Fixed64.One;
+ var pos5 = 5 * Fixed64.One;
+ for (int i = 0; i < 2048; i++)
+ {
+ var v = rng.NextFixed64(neg5, pos5);
+ Assert.True(v >= neg5 && v < pos5);
+ }
+ }
+ }
+}
\ No newline at end of file