GameRandom.cs 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. using System;
  2. /// <summary>
  3. /// Deterministic PRNG for game-critical randomness (deck shuffling, sync keys, coin flips).
  4. /// Uses Xoshiro256** with SplitMix64 seeding for a 256-bit internal state.
  5. /// Both clients seed with the same value to produce identical sequences.
  6. /// </summary>
  7. public static class GameRandom
  8. {
  9. private static ulong s0, s1, s2, s3;
  10. private static bool initialized = false;
  11. /// <summary>
  12. /// Seed the PRNG with a 64-bit value. Uses SplitMix64 to expand
  13. /// the seed into 256 bits of Xoshiro256** state.
  14. /// </summary>
  15. public static void Seed(long seed)
  16. {
  17. ulong s = (ulong)seed;
  18. s0 = SplitMix64(ref s);
  19. s1 = SplitMix64(ref s);
  20. s2 = SplitMix64(ref s);
  21. s3 = SplitMix64(ref s);
  22. // Ensure state is not all-zero (degenerate case)
  23. if (s0 == 0 && s1 == 0 && s2 == 0 && s3 == 0)
  24. s0 = 1;
  25. initialized = true;
  26. }
  27. /// <summary>
  28. /// Returns a random int in [min, exclusiveMax) with no modulo bias.
  29. /// Uses rejection sampling.
  30. /// </summary>
  31. public static int Range(int min, int exclusiveMax)
  32. {
  33. if (!initialized)
  34. throw new InvalidOperationException("GameRandom has not been seeded. Call GameRandom.Seed() first.");
  35. if (exclusiveMax <= min)
  36. return min;
  37. uint range = (uint)(exclusiveMax - min);
  38. if (range == 1)
  39. return min;
  40. // Rejection sampling to eliminate modulo bias
  41. // Threshold: values below this are in the biased zone
  42. uint threshold = (uint)((0x100000000UL - range) % range);
  43. uint raw;
  44. do
  45. {
  46. raw = NextUInt32();
  47. } while (raw < threshold);
  48. return min + (int)(raw % range);
  49. }
  50. /// <summary>
  51. /// Returns a random float in [min, max).
  52. /// </summary>
  53. public static float Range(float min, float max)
  54. {
  55. if (!initialized)
  56. throw new InvalidOperationException("GameRandom has not been seeded. Call GameRandom.Seed() first.");
  57. // Use 24 bits of randomness for float precision (IEEE 754 single has 23-bit mantissa)
  58. float t = (NextUInt32() >> 8) * (1.0f / (1 << 24));
  59. return min + (max - min) * t;
  60. }
  61. /// <summary>
  62. /// Returns true with the given probability [0..1].
  63. /// </summary>
  64. public static bool Probability(float p)
  65. {
  66. if (p >= 1f) return true;
  67. if (p <= 0f) return false;
  68. return Range(0f, 1f) <= p;
  69. }
  70. // --- Internal: Xoshiro256** ---
  71. private static ulong NextState()
  72. {
  73. // xoshiro256** result calculation
  74. ulong result = RotateLeft(s1 * 5, 7) * 9;
  75. ulong t = s1 << 17;
  76. s2 ^= s0;
  77. s3 ^= s1;
  78. s1 ^= s2;
  79. s0 ^= s3;
  80. s2 ^= t;
  81. s3 = RotateLeft(s3, 45);
  82. return result;
  83. }
  84. private static uint NextUInt32()
  85. {
  86. return (uint)(NextState() >> 32);
  87. }
  88. private static ulong RotateLeft(ulong x, int k)
  89. {
  90. return (x << k) | (x >> (64 - k));
  91. }
  92. // --- Internal: SplitMix64 (for seed expansion) ---
  93. private static ulong SplitMix64(ref ulong state)
  94. {
  95. ulong z = (state += 0x9E3779B97F4A7C15UL);
  96. z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL;
  97. z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL;
  98. return z ^ (z >> 31);
  99. }
  100. }