Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

game_slots.gno

6.29 Kb · 240 lines
  1package game31
  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}