aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/slots.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 05:31:09 -0400
committerhistoria <[not public]>2026-09-18 05:31:09 -0400
commit153acb7f644d755c45533b7c5311e3ffa47e8973 (patch)
tree7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/casino/slots.go
parent87d08f0151e643951769e323995f1c5ed89ee200 (diff)
downloadthehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/casino/slots.go')
-rw-r--r--internal/casino/slots.go31
1 files changed, 24 insertions, 7 deletions
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
}