aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/manager.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/manager.go')
-rw-r--r--internal/casino/manager.go47
1 files changed, 43 insertions, 4 deletions
diff --git a/internal/casino/manager.go b/internal/casino/manager.go
index 4798217..8b87147 100644
--- a/internal/casino/manager.go
+++ b/internal/casino/manager.go
@@ -1,6 +1,8 @@
package casino
import (
+ crand "crypto/rand"
+ "encoding/binary"
"fmt"
"math/rand"
"reflect"
@@ -14,13 +16,43 @@ type Manager struct {
tables map[string]*Table
machines map[string]*Machine
rng *rand.Rand
+ // seed is the seed the manager's stream was created with, so a test can
+ // assert that the entropy path really drew a fresh one.
+ seed int64
}
-func NewManager(seed int64) *Manager {
+// NewManager returns a Manager with DETERMINISTIC per-game RNG streams
+// derived from seed. It is the test seam: a fixed seed must give fixed
+// shoes, reels, and spins. Production uses NewManagerEntropy.
+func NewManager(seed int64) *Manager { return newManager(seed) }
+
+// NewManagerEntropy returns a Manager whose per-game RNG streams are seeded
+// from crypto/rand. Casino outcomes must not be reconstructible from the
+// clock: a wall-clock seed lets anyone who knows (or brute-forces) the
+// startup nanosecond replay every shoe and reel stream, so a certified
+// 0.97-RTP machine becomes an unlimited-credit one. The streams themselves
+// stay math/rand (unbiased Intn/Shuffle) and each game keeps its own.
+func NewManagerEntropy() *Manager { return newManager(entropySeed()) }
+
+// entropySeed draws a fresh 63-bit seed from crypto/rand. A broken entropy
+// source is fatal instead of silently degrading to a reconstructible clock
+// seed: the casino's game integrity depends on it.
+func entropySeed() int64 {
+ var b [8]byte
+ if _, err := crand.Read(b[:]); err != nil {
+ panic(fmt.Sprintf("casino entropy seed: %v", err))
+ }
+ // A non-negative 63-bit value: math/rand accepts any int64 seed, and a
+ // non-negative seed prints cleanly in diagnostics.
+ return int64(binary.LittleEndian.Uint64(b[:]) >> 1)
+}
+
+func newManager(seed int64) *Manager {
return &Manager{
tables: make(map[string]*Table),
machines: make(map[string]*Machine),
rng: rand.New(rand.NewSource(seed)),
+ seed: seed,
}
}
@@ -294,9 +326,16 @@ func (m *Manager) MachineBusy(name string) bool {
}
// RefundAll resolves every outstanding wager by refunding committed stakes
-// to their players. Graceful shutdown uses this: rounds cannot settle, so
-// no player may lose a stake to the shutdown itself. Refunds go through the
-// wallet (durable, journaled) and return the refund events for delivery.
+// to their players and then closing each game's round. Graceful shutdown and
+// room eviction use this: rounds cannot settle, so no player may lose a
+// stake to the shutdown itself. Refunds go through the wallet (durable,
+// journaled) and the refund events are returned for delivery.
+//
+// It is deliberately NOT a way to clear live tables: every game discards
+// the round it refunded (seats keep their last bet only for "rebet", but
+// phases, hands, spins, and timers are reset), so a refunded round cannot
+// settle later and a second call refunds nothing. A caller that wants the
+// games to keep playing must not use this.
func (m *Manager) RefundAll() []Event {
var events []Event
for _, table := range m.snapshotTables() {