package sim import ( "encoding/binary" "github.com/drjones/quantum-arcade/pkg/fixed" ) // RNG is a deterministic xoshiro256** generator seeded from 32 bytes. // It uses only integer operations, so a browser replaying a round reproduces // the server's stream exactly. // // This is the expansion function, not the entropy source: the seed itself comes // from the commit-reveal protocol, which is what makes outcomes unriggable. type RNG struct { state [4]uint64 } // NewRNG creates a reproducible generator from a 32-byte seed. // // The seed is run through SplitMix64 rather than copied into the state // directly. Copying directly leaves the first output depending only on // state[1], so seeds differing in other bytes produce identical first draws — // which would make CrashPoint blind to most of its own seed. func NewRNG(seed [32]byte) *RNG { // Fold every seed byte into a single accumulator first, so all 32 bytes // influence all four state words. acc := uint64(0x9E3779B97F4A7C15) for i := 0; i < 4; i++ { acc ^= binary.LittleEndian.Uint64(seed[i*8 : i*8+8]) acc = splitMix64(&acc) } r := &RNG{} for i := 0; i < 4; i++ { r.state[i] = splitMix64(&acc) } // An all-zero state is a fixed point of the recurrence. SplitMix64 makes // this vanishingly unlikely, but the guard costs nothing. if r.state[0]|r.state[1]|r.state[2]|r.state[3] == 0 { r.state[0] = 0x9E3779B97F4A7C15 } return r } // splitMix64 advances x and returns a well-mixed 64-bit value. Every input bit // affects every output bit, which is the property the state expansion needs. func splitMix64(x *uint64) uint64 { *x += 0x9E3779B97F4A7C15 z := *x z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9 z = (z ^ (z >> 27)) * 0x94D049BB133111EB return z ^ (z >> 31) } // Uint64 returns the next 64 bits of the stream. func (r *RNG) Uint64() uint64 { s := &r.state result := rotl(s[1]*5, 7) * 9 t := s[1] << 17 s[2] ^= s[0] s[3] ^= s[1] s[1] ^= s[2] s[0] ^= s[3] s[2] ^= t s[3] = rotl(s[3], 45) return result } func rotl(x uint64, k uint) uint64 { return (x << k) | (x >> (64 - k)) } // Unit returns a fixed-point value uniformly distributed over [0, 1). // Taking the top 32 bits places them exactly in the fractional field. func (r *RNG) Unit() fixed.F { return fixed.F(r.Uint64() >> 32) }