diff options
| author | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
| commit | 153acb7f644d755c45533b7c5311e3ffa47e8973 (patch) | |
| tree | 7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/casino/manager.go | |
| parent | 87d08f0151e643951769e323995f1c5ed89ee200 (diff) | |
| download | thehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz | |
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/casino/manager.go')
| -rw-r--r-- | internal/casino/manager.go | 47 |
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() { |
