aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/hardening_casino_rng_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/hardening_casino_rng_test.go')
-rw-r--r--internal/casino/hardening_casino_rng_test.go75
1 files changed, 75 insertions, 0 deletions
diff --git a/internal/casino/hardening_casino_rng_test.go b/internal/casino/hardening_casino_rng_test.go
new file mode 100644
index 0000000..2d44441
--- /dev/null
+++ b/internal/casino/hardening_casino_rng_test.go
@@ -0,0 +1,75 @@
+package casino
+
+import "testing"
+
+// RNG seams: outcomes must not be replayable from a clock, while tests keep
+// a deterministic, injectable seed.
+
+func TestNewManagerSeedIsADeterministicSeam(t *testing.T) {
+ cfg := MachineConfig{ID: "slots", Game: GameSlots}
+ same := []*Machine{
+ NewManager(7).EnsureMachine(1, cfg),
+ NewManager(7).EnsureMachine(1, cfg),
+ }
+ other := NewManager(8).EnsureMachine(1, cfg)
+
+ first := machineDraws(same[0])
+ if got := machineDraws(same[1]); !sameDraws(first, got) {
+ t.Fatalf("the same seed produced different slot streams: %v vs %v", first, got)
+ }
+ if got := machineDraws(other); sameDraws(first, got) {
+ t.Fatalf("different seeds produced the same slot stream: %v", got)
+ }
+}
+
+func TestNewManagerEntropySeedsFromCryptoRand(t *testing.T) {
+ if seed := entropySeed(); seed < 0 {
+ t.Fatalf("entropy seed %d is negative", seed)
+ }
+ seen := make(map[int64]bool)
+ for i := 0; i < 4; i++ {
+ m := NewManagerEntropy()
+ if m.seed < 0 {
+ t.Fatalf("NewManagerEntropy seed %d is negative", m.seed)
+ }
+ seen[m.seed] = true
+ }
+ if len(seen) < 2 {
+ t.Fatalf("NewManagerEntropy reused one seed across %d managers", len(seen))
+ }
+}
+
+// Each game gets its own stream seeded from the manager's: no two tables or
+// machines may share reel results.
+func TestManagerGamesKeepIndependentStreams(t *testing.T) {
+ m := NewManager(3)
+ first := m.EnsureMachine(1, MachineConfig{ID: "slots_a", Game: GameSlots})
+ second := m.EnsureMachine(1, MachineConfig{ID: "slots_b", Game: GameSlots})
+ if first == nil || second == nil {
+ t.Fatal("machines were not created")
+ }
+ if sameDraws(machineDraws(first), machineDraws(second)) {
+ t.Fatal("two machines share one RNG stream")
+ }
+}
+
+// machineDraws consumes n values from a game's own stream.
+func machineDraws(m *Machine) []int64 {
+ draws := make([]int64, 8)
+ for i := range draws {
+ draws[i] = m.rng.Int63()
+ }
+ return draws
+}
+
+func sameDraws(a, b []int64) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+ return true
+}