feat(sim): add deterministic RNG and crash curve

Seed expansion uses SplitMix64 so all 32 seed bytes affect the stream;
copying the seed directly into xoshiro state left the first draw
dependent only on bytes 8-15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-08-05 02:48:17 +00:00
parent 9413537251
commit 8fccbb2a9a
4 changed files with 301 additions and 0 deletions

51
pkg/sim/rng_test.go Normal file
View File

@@ -0,0 +1,51 @@
package sim
import "testing"
func TestRNGIsDeterministic(t *testing.T) {
var seed [32]byte
copy(seed[:], "quantum-arcade-test-seed")
a, b := NewRNG(seed), NewRNG(seed)
for i := 0; i < 1000; i++ {
if x, y := a.Uint64(), b.Uint64(); x != y {
t.Fatalf("iteration %d: %d != %d", i, x, y)
}
}
}
func TestDifferentSeedsDiverge(t *testing.T) {
var s1, s2 [32]byte
copy(s1[:], "seed-one")
copy(s2[:], "seed-two")
a, b := NewRNG(s1), NewRNG(s2)
same := 0
for i := 0; i < 100; i++ {
if a.Uint64() == b.Uint64() {
same++
}
}
if same > 1 {
t.Fatalf("streams collided %d times in 100 draws", same)
}
}
func TestZeroSeedDoesNotDegenerate(t *testing.T) {
var seed [32]byte // all zeros
r := NewRNG(seed)
first := r.Uint64()
if first == 0 && r.Uint64() == 0 {
t.Fatal("zero seed produced a degenerate all-zero stream")
}
}
func TestUnitInRange(t *testing.T) {
var seed [32]byte
seed[0] = 9
r := NewRNG(seed)
for i := 0; i < 10000; i++ {
u := r.Unit()
if u < 0 || u >= 1<<32 {
t.Fatalf("Unit() = %v out of [0,1)", u)
}
}
}