From 153acb7f644d755c45533b7c5311e3ffa47e8973 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 05:31:09 -0400 Subject: fix: harden engine across casino, economy, concurrency, security, and content --- internal/casino/slots.go | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) (limited to 'internal/casino/slots.go') diff --git a/internal/casino/slots.go b/internal/casino/slots.go index 075050c..64caf85 100644 --- a/internal/casino/slots.go +++ b/internal/casino/slots.go @@ -23,12 +23,22 @@ type SlotProfile struct { } type SlotSpin struct { - Grid [3][5]string + Grid [slotGridRows][slotGridReels]string Payout float64 ScatterCount int Wins []SlotWin } +// The runtime slot evaluator is pinned to this grid: SlotSpin/SlotSnapshot +// hold it, generateSlotSpinWithProfile fills it, and matchingLength and +// stoppedScatters evaluate it. CalculateRTP/ValidateRTP are generic over +// SlotProfile.Rows/Reels/Ways, so SlotProfile.Validate rejects any other +// geometry (see config_validate.go) to keep the RTP gate honest. +const ( + slotGridRows = 3 + slotGridReels = 5 +) + type SlotWin struct { Symbol string Length int @@ -72,10 +82,12 @@ func (p SlotProfile) Normalize() SlotProfile { } func generateSlotSpinWithProfile(rng *rand.Rand, bet int, profile SlotProfile) *SlotSpin { + // SlotProfile.Normalize supplies the defaults; Validate rejects a + // geometry this grid cannot hold, so the bounds are exactly the grid. p := profile.Normalize() spin := &SlotSpin{} - for reel := 0; reel < p.Reels && reel < 5; reel++ { - for row := 0; row < p.Rows && row < 3; row++ { + for reel := 0; reel < p.Reels && reel < slotGridReels; reel++ { + for row := 0; row < p.Rows && row < slotGridRows; row++ { symbol := weightedSymbol(rng, p.Symbols) spin.Grid[row][reel] = symbol if symbol == p.FreeSpinTrigger { @@ -110,7 +122,7 @@ func weightedSymbol(rng *rand.Rand, symbols []SlotSymbol) string { return symbols[len(symbols)-1].ID } -func evaluateSlot(grid [3][5]string, bet int, profile SlotProfile) (float64, []SlotWin) { +func evaluateSlot(grid [slotGridRows][slotGridReels]string, bet int, profile SlotProfile) (float64, []SlotWin) { p := profile.Normalize() if bet <= 0 { return 0, nil @@ -128,12 +140,12 @@ func evaluateSlot(grid [3][5]string, bet int, profile SlotProfile) (float64, []S return total, wins } -func matchingLength(grid [3][5]string, symbol string) (int, int) { +func matchingLength(grid [slotGridRows][slotGridReels]string, symbol string) (int, int) { ways := 1 length := 0 - for reel := 0; reel < 5; reel++ { + for reel := 0; reel < slotGridReels; reel++ { count := 0 - for row := 0; row < 3; row++ { + for row := 0; row < slotGridRows; row++ { if grid[row][reel] == symbol { count++ } @@ -204,6 +216,11 @@ func binomial(n, k int) float64 { } func ValidateRTP(profile SlotProfile, tolerance float64) error { + // A profile the runtime cannot play must never be certified: its RTP + // would describe a different game than the one actually paid. + if err := profile.Validate(); err != nil { + return err + } if profile.TargetRTP <= 0 { return nil } -- cgit v1.2.3