diff --git a/internal/engine/rng.go b/internal/engine/rng.go new file mode 100644 index 0000000..009148c --- /dev/null +++ b/internal/engine/rng.go @@ -0,0 +1,16 @@ +// Package engine implements the agent-based spread model: build a social +// network, run an independent cascade over it, and measure the effect of +// education strategies. It is pure: no I/O, no web dependencies, and all +// randomness flows from seeds in the config, so identical inputs always +// produce identical runs. +package engine + +import "math/rand/v2" + +// newRand returns a deterministic random stream for the given seed. The +// engine never touches global random state; every source of randomness +// (graph build, edge thresholds, education sampling) gets its own seeded +// stream so each can be varied independently. +func newRand(seed uint64) *rand.Rand { + return rand.New(rand.NewPCG(seed, 0)) +} diff --git a/internal/engine/rng_test.go b/internal/engine/rng_test.go new file mode 100644 index 0000000..0ba8de4 --- /dev/null +++ b/internal/engine/rng_test.go @@ -0,0 +1,26 @@ +package engine + +import "testing" + +// Determinism is a locked project decision: the same seed must always yield +// the same stream, and different seeds must not collide. With fixed seeds +// these assertions are exact, not probabilistic. + +func TestNewRandSameSeedSameStream(t *testing.T) { + a, b := newRand(17), newRand(17) + for i := range 100 { + if got, want := a.Float64(), b.Float64(); got != want { + t.Fatalf("draw %d: streams diverged: %v != %v", i, got, want) + } + } +} + +func TestNewRandDifferentSeedsDiffer(t *testing.T) { + a, b := newRand(17), newRand(18) + for range 100 { + if a.Float64() != b.Float64() { + return // diverged, as expected + } + } + t.Fatal("seeds 17 and 18 produced 100 identical draws") +}