game_slots.gno
6.29 Kb · 240 lines
1package game33
2
3import (
4 "crypto/sha256"
5 "strconv"
6)
7
8// ── Slots engine: every reel, line, scatter and Hold & Spin tile comes from the
9// round's combined seed. The client renders what the realm stored and re-derives
10// it with the same rules (src/lib/game/slots-engine.ts mirrors this file).
11//
12// Stream: block i = sha256(serverSeed ":" clientSecret ":" roundId ":slots:" i),
13// bytes consumed in order. Reel c stops at uint32(next 4 bytes) % 40 and shows
14// strip[(stop+r) % 40] for rows r = 0..3. Grid cells are indexed c*4+r.
15//
16// Paytable (see the RTP math in .bot-notes/casino/CASINO_100_PLAN.md):
17// 25 lines pay left to right in units of the line bet (bet/25; bets are a
18// multiple of 25 so nothing is rounded); wild substitutes; the anchor is the
19// first symbol that is not wild/scatter/vault (an all-wild line pays wild).
20// 3/4/5 scatters anywhere pay 2/10/50× the bet and grant 10/12/15 free spins.
21// 3+ vaults start Hold & Spin. Every win of a round is capped at 500× the bet.
22
23const (
24 slotsReels = 5
25 slotsRows = 4
26 slotsCells = 20
27 slotsStripLen = 40
28 slotsBetStep = uint64(25) // bet must be a multiple of the 25 lines
29 slotsMaxWinMult = uint64(500)
30
31 symWild = byte(0)
32 symScatter = byte(8)
33 symVault = byte(9)
34
35 // Hold & Spin
36 hsLandBelow = byte(8) // a free cell lands a coin when its byte < 8 (3.125%)
37 hsRespins = 3
38
39 // Hold & Spin tile codes (anything < 100 is a multiple of the bet)
40 hsMini = uint16(101)
41 hsMinor = uint16(102)
42 hsMajor = uint16(103)
43 hsGrand = uint16(104)
44)
45
46// One strip for all reels: wild 1, seven 3, diamond 4, trophy 5, star 6,
47// clover 6, flame 7, coin 6, scatter 1, vault 1 (digits = symbol ids).
48const slotsStrip = "8645732106457321645793216457326457364576"
49
50var slotsSymbolNames = [10]string{"wild", "seven", "diamond", "trophy", "star", "clover", "flame", "coin", "scatter", "vault"}
51
52// line-bet multiples for 3/4/5 of a kind, symbols 0..7
53var slotsPays = [8][3]uint64{
54 {60, 250, 1000}, // wild
55 {40, 150, 500}, // seven
56 {30, 100, 300}, // diamond
57 {25, 75, 200}, // trophy
58 {15, 40, 100}, // star
59 {12, 30, 80}, // clover
60 {10, 25, 60}, // flame
61 {8, 20, 50}, // coin
62}
63
64// same 25 lines as src/lib/game/slots.ts PAYLINES (row per reel)
65var slotsPaylines = [25][5]byte{
66 {1, 1, 1, 1, 1}, {2, 2, 2, 2, 2}, {0, 0, 0, 0, 0}, {3, 3, 3, 3, 3},
67 {0, 1, 2, 1, 0}, {3, 2, 1, 2, 3}, {0, 1, 3, 1, 0}, {3, 2, 0, 2, 3},
68 {0, 1, 2, 3, 3}, {3, 2, 1, 0, 0}, {0, 0, 1, 2, 3}, {3, 3, 2, 1, 0},
69 {1, 2, 1, 2, 1}, {2, 1, 2, 1, 2}, {1, 0, 1, 2, 1}, {2, 3, 2, 1, 2},
70 {1, 0, 0, 0, 1}, {2, 3, 3, 3, 2}, {0, 2, 3, 2, 0}, {3, 1, 0, 1, 3},
71 {1, 1, 2, 1, 1}, {2, 2, 1, 2, 2}, {0, 1, 0, 1, 0}, {3, 2, 3, 2, 3},
72 {0, 3, 0, 3, 0},
73}
74
75var (
76 slotsScatterPay = [6]uint64{0, 0, 0, 2, 10, 50} // × bet, by scatter count
77 slotsFreeSpins = [6]uint64{0, 0, 0, 10, 12, 15}
78)
79
80type slotsStream struct {
81 prefix string
82 ctr int
83 buf [32]byte
84 pos int
85}
86
87func newSlotsStream(serverSeed, clientSecret, roundId string) *slotsStream {
88 return &slotsStream{prefix: serverSeed + ":" + clientSecret + ":" + roundId + ":slots:", pos: 32}
89}
90
91func (s *slotsStream) next() byte {
92 if s.pos >= 32 {
93 s.buf = sha256.Sum256([]byte(s.prefix + strconv.Itoa(s.ctr)))
94 s.ctr++
95 s.pos = 0
96 }
97 b := s.buf[s.pos]
98 s.pos++
99 return b
100}
101
102func (s *slotsStream) u32() uint32 {
103 return uint32(s.next())<<24 | uint32(s.next())<<16 | uint32(s.next())<<8 | uint32(s.next())
104}
105
106// slotsSpinGrid draws the 5 reel stops and returns the 20 visible symbols.
107func slotsSpinGrid(s *slotsStream) [slotsCells]byte {
108 var g [slotsCells]byte
109 for c := 0; c < slotsReels; c++ {
110 stop := int(s.u32() % slotsStripLen)
111 for r := 0; r < slotsRows; r++ {
112 g[c*slotsRows+r] = slotsStrip[(stop+r)%slotsStripLen] - '0'
113 }
114 }
115 return g
116}
117
118// slotsLineMult = Σ line pays in line-bet units, and how many lines paid.
119func slotsLineMult(g *[slotsCells]byte) (uint64, int) {
120 total := uint64(0)
121 lines := 0
122 for l := 0; l < 25; l++ {
123 var sym [5]byte
124 anchor := byte(255)
125 for c := 0; c < 5; c++ {
126 sym[c] = g[c*slotsRows+int(slotsPaylines[l][c])]
127 if anchor == 255 && sym[c] != symWild && sym[c] != symScatter && sym[c] != symVault {
128 anchor = sym[c]
129 }
130 }
131 if anchor == 255 {
132 if sym[0] != symWild {
133 continue
134 }
135 anchor = symWild
136 }
137 m := 0
138 for c := 0; c < 5; c++ {
139 if sym[c] == anchor || sym[c] == symWild {
140 m++
141 } else {
142 break
143 }
144 }
145 if m >= 3 {
146 total += slotsPays[anchor][m-3]
147 lines++
148 }
149 }
150 return total, lines
151}
152
153func slotsCount(g *[slotsCells]byte, sym byte) int {
154 n := 0
155 for i := 0; i < slotsCells; i++ {
156 if g[i] == sym {
157 n++
158 }
159 }
160 return n
161}
162
163// hsDrawValue: 1× 50%, 2× 28%, 3× 12%, 5× 6%, MINI 3%, MINOR 0.9%, MAJOR 0.08%, GRAND 0.02%.
164func hsDrawValue(s *slotsStream) uint16 {
165 v := s.u32() % 10000
166 switch {
167 case v < 5000:
168 return 1
169 case v < 7800:
170 return 2
171 case v < 9000:
172 return 3
173 case v < 9600:
174 return 5
175 case v < 9900:
176 return hsMini
177 case v < 9990:
178 return hsMinor
179 case v < 9998:
180 return hsMajor
181 }
182 return hsGrand
183}
184
185// slotsHoldAndSpin resolves the whole bonus from the stream: the vault cells
186// start locked with a value each; then while respins remain and the board isn't
187// full, every empty cell (in index order) lands a coin when its byte < 8, and a
188// landing resets the respins to 3. Returns the 20 tiles (0 = empty), how many
189// respins were played, and whether the board filled (blackout = GRAND).
190func slotsHoldAndSpin(s *slotsStream, g *[slotsCells]byte) ([slotsCells]uint16, int, bool) {
191 var tiles [slotsCells]uint16
192 locked := 0
193 for i := 0; i < slotsCells; i++ {
194 if g[i] == symVault {
195 tiles[i] = hsDrawValue(s)
196 locked++
197 }
198 }
199 respins := hsRespins
200 played := 0
201 for respins > 0 && locked < slotsCells {
202 played++
203 landed := false
204 for i := 0; i < slotsCells; i++ {
205 if tiles[i] != 0 {
206 continue
207 }
208 if s.next() < hsLandBelow {
209 tiles[i] = hsDrawValue(s)
210 locked++
211 landed = true
212 }
213 }
214 if landed {
215 respins = hsRespins
216 } else {
217 respins--
218 }
219 }
220 return tiles, played, locked == slotsCells
221}
222
223func slotsGridString(g *[slotsCells]byte) string {
224 b := make([]byte, slotsCells)
225 for i := 0; i < slotsCells; i++ {
226 b[i] = '0' + g[i]
227 }
228 return string(b)
229}
230
231func hsTilesString(t *[slotsCells]uint16) string {
232 out := ""
233 for i := 0; i < slotsCells; i++ {
234 if i > 0 {
235 out += "."
236 }
237 out += strconv.Itoa(int(t[i]))
238 }
239 return out
240}