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_casino_bonus.gno

10.05 Kb · 301 lines
  1package game31
  2
  3import (
  4	"strconv"
  5	"time"
  6
  7	"chain"
  8
  9	"gno.land/p/nt/avl/v0"
 10)
 11
 12// ── Slots bonus features, all settled by the realm ──────────────────────────
 13//
 14// Free spins: 3/4/5 scatters on a paid spin grant 10/12/15 free spins at that
 15// spin's bet. The grant is a realm record (one open grant per player). The
 16// triggering spin keeps its 500× reservation for the whole feature, minus what
 17// it already paid, so every free spin can always be paid in full. A free spin is
 18// its own two-sided round (PlaceCasinoFreeSpin takes no stake); its wins are
 19// doubled, it pays from that reservation, scatters don't retrigger and vaults
 20// don't start Hold & Spin. The grant never expires for the player.
 21//
 22// Hold & Spin: 3+ vaults on a paid spin. Resolved inside that spin's settlement
 23// from the same seed stream (game_slots.gno). Coin tiles pay 1/2/3/5× the bet,
 24// MINI 20×, MINOR 75× (all under the 500× cap). The first MAJOR / GRAND tile
 25// pays that jackpot pool (extra ones pay as MINOR); a full board (20 tiles) also
 26// awards GRAND.
 27//
 28// MAJOR / GRAND jackpots: real pools per coin, fed with 1.0% / 0.5% of every
 29// paid slots stake (fractions carried exactly). A hit pays
 30// pool × min(1, bet / refBet); the rest stays. A pool can never pay more than
 31// it holds. After a hit the pool is topped back up to its reset seed from the
 32// house pool, only while the free house pool is ≥ 20× that top-up. The admin
 33// can add its own coins to a jackpot but nobody can withdraw one.
 34
 35var (
 36	casinoGrants  avl.Tree // addr → *freeSpinGrant
 37	casinoJackpot avl.Tree // coin:tier → pool (string)
 38	casinoJpDust  avl.Tree // coin:tier → contribution remainder in bet×bps units (string)
 39	casinoJpSeed  avl.Tree // coin:tier → reset seed (string)
 40	casinoJpRef   avl.Tree // coin → reference bet (string)
 41)
 42
 43const (
 44	freeSpinMult        = uint64(2)
 45	jpMajorBps          = uint64(100) // 1.0% of each paid slots stake
 46	jpGrandBps          = uint64(50)  // 0.5%
 47	jpDefaultRefBet     = uint64(100)
 48	jpResetFreeMultiple = uint64(20)
 49	hsMiniMult          = uint64(20)
 50	hsMinorMult         = uint64(75)
 51)
 52
 53type freeSpinGrant struct {
 54	addr, coin string
 55	bet        uint64
 56	left       uint64
 57	total      uint64
 58	capLeft    uint64 // still reserved in casinoReserved for this feature
 59	won        uint64
 60	inRound    string
 61	fromRound  string
 62	grantedAt  int64
 63}
 64
 65func getGrant(addr string) *freeSpinGrant {
 66	if !casinoGrants.Has(addr) {
 67		return nil
 68	}
 69	return casinoGrants.Get(addr).(*freeSpinGrant)
 70}
 71
 72func jpKey(coin, tier string) string { return coin + ":" + tier }
 73
 74func jpRefBet(coin string) uint64 {
 75	if v := casGet(&casinoJpRef, coin); v > 0 {
 76		return v
 77	}
 78	return jpDefaultRefBet
 79}
 80
 81// casinoJackpotContribute moves 1.0% / 0.5% of a paid slots stake from the house
 82// pool into the MAJOR / GRAND pools (remainders carried, nothing rounded away).
 83func casinoJackpotContribute(coin string, amt uint64) {
 84	for _, t := range [2]struct {
 85		tier string
 86		bps  uint64
 87	}{{"major", jpMajorBps}, {"grand", jpGrandBps}} {
 88		k := jpKey(coin, t.tier)
 89		dust := casAdd(casGet(&casinoJpDust, k), casMul(amt, t.bps))
 90		units := dust / 10000
 91		if units == 0 {
 92			casSet(&casinoJpDust, k, dust)
 93			continue
 94		}
 95		pool := casGet(&casinoPool, coin)
 96		if pool < casAdd(casGet(&casinoReserved, coin), units) {
 97			casSet(&casinoJpDust, k, dust) // house can't spare it right now: keep it owed
 98			continue
 99		}
100		casSet(&casinoJpDust, k, dust%10000)
101		casSet(&casinoPool, coin, pool-units)
102		casSet(&casinoJackpot, k, casAdd(casGet(&casinoJackpot, k), units))
103		day, snap, paid, recv := casinoDayState(coin)
104		setCasinoDay(coin, day, snap, casAdd(paid, units), recv)
105	}
106}
107
108// casinoJackpotPay pays a jackpot tier to addr: pool × min(1, bet/refBet).
109func casinoJackpotPay(addr, coin, tier string, bet uint64) uint64 {
110	k := jpKey(coin, tier)
111	pool := casGet(&casinoJackpot, k)
112	ref := jpRefBet(coin)
113	pay := pool
114	if bet < ref {
115		pay = casMulDiv(pool, bet, ref)
116	}
117	casSet(&casinoJackpot, k, pool-pay)
118	casCredit(addr, coin, pay)
119	chain.Emit("CasinoJackpotWon", "player", addr, "coin", coin, "tier", tier, "amount", casU(pay))
120	// Top back up to the reset seed from the house pool, only if the house can easily afford it.
121	seed := casGet(&casinoJpSeed, k)
122	left := pool - pay
123	if seed > left {
124		need := seed - left
125		house := casGet(&casinoPool, coin)
126		reserved := casGet(&casinoReserved, coin)
127		if house > reserved && house-reserved >= casMul(need, jpResetFreeMultiple) {
128			casSet(&casinoPool, coin, house-need)
129			casSet(&casinoJackpot, k, seed)
130			day, snap, paid, recv := casinoDayState(coin)
131			setCasinoDay(coin, day, snap, casAdd(paid, need), recv)
132		}
133	}
134	return pay
135}
136
137// slotsHSPayout turns Hold & Spin tiles into bet multiples (under the cap) and
138// the jackpot tiers won (paid from their pools).
139func slotsHSPayout(tiles *[slotsCells]uint16, full bool) (uint64, bool, bool) {
140	mult := uint64(0)
141	major, grand := false, false
142	for i := 0; i < slotsCells; i++ {
143		v := tiles[i]
144		switch {
145		case v == 0:
146		case v < 100:
147			mult += uint64(v)
148		case v == hsMini:
149			mult += hsMiniMult
150		case v == hsMinor:
151			mult += hsMinorMult
152		case v == hsMajor && !major:
153			major = true
154		case v == hsGrand && !grand:
155			grand = true
156		default: // a second MAJOR / GRAND in the same bonus pays as MINOR
157			mult += hsMinorMult
158		}
159	}
160	if full && !grand {
161		grand = true
162	}
163	return mult, major, grand
164}
165
166// PlaceCasinoFreeSpin uses one free spin of the caller's grant in a committed
167// round (no stake). One free spin in flight at a time.
168func PlaceCasinoFreeSpin(cur realm, roundId string, clientHash string) {
169	requireNotPaused()
170	caller := cur.Previous().Address().String()
171	if isBlacklisted(caller) {
172		panic("address banned")
173	}
174	r := mustCasinoRound(roundId)
175	if r.player != caller {
176		panic("this round belongs to another player")
177	}
178	if r.status != "committed" {
179		panic("betting is closed for this round")
180	}
181	if time.Now().Unix()-r.committedAt >= casinoBetWindowSec {
182		panic("round expired")
183	}
184	g := getGrant(caller)
185	if g == nil || g.left == 0 {
186		panic("no free spins left")
187	}
188	if g.inRound != "" {
189		panic("a free spin is already in progress (round " + g.inRound + ")")
190	}
191	if !casIsHex64(clientHash) {
192		panic("client hash must be sha256 hex")
193	}
194	g.inRound = roundId
195	casinoBets.Set(roundId+":"+caller, &casinoBet{addr: caller, game: "slots", coin: g.coin, choice: "free", clientHash: clientHash, amount: g.bet, freeSpin: true})
196	r.status = "bet"
197	r.betAt = time.Now().Unix()
198	r.bets = 1
199}
200
201// settleFreeSpin pays a free spin's win out of its grant's reservation.
202func settleFreeSpin(b *casinoBet, win uint64) uint64 {
203	g := getGrant(b.addr)
204	if g == nil {
205		panic("free spin grant missing")
206	}
207	if win > g.capLeft {
208		win = g.capLeft
209		b.capped = true
210	}
211	if win > 0 {
212		casSet(&casinoReserved, b.coin, casSub(casGet(&casinoReserved, b.coin), win))
213		casSet(&casinoPool, b.coin, casSub(casGet(&casinoPool, b.coin), win))
214		day, snap, paid, recv := casinoDayState(b.coin)
215		setCasinoDay(b.coin, day, snap, casAdd(paid, win), recv)
216		casCredit(b.addr, b.coin, win)
217	}
218	g.capLeft -= win
219	g.won += win
220	g.left--
221	g.inRound = ""
222	if g.left == 0 {
223		casSet(&casinoReserved, b.coin, casSub(casGet(&casinoReserved, b.coin), g.capLeft))
224		casinoGrants.Remove(b.addr)
225		chain.Emit("CasinoFreeSpinsDone", "player", b.addr, "coin", b.coin, "won", casU(g.won))
226	}
227	return win
228}
229
230// FreeSpinGrant reports a player's open free spins (null when none).
231func FreeSpinGrant(addr string) string {
232	g := getGrant(addr)
233	if g == nil {
234		return "null"
235	}
236	return `{"coin":"` + g.coin + `","bet":` + casU(g.bet) + `,"left":` + casU(g.left) + `,"total":` + casU(g.total) +
237		`,"mult":` + casU(freeSpinMult) + `,"capLeft":` + casU(g.capLeft) + `,"won":` + casU(g.won) +
238		`,"inRound":"` + g.inRound + `","fromRound":"` + g.fromRound + `"}`
239}
240
241// CasinoJackpots reports the real MAJOR / GRAND pools and how they pay.
242func CasinoJackpots(coinId string) string {
243	return `{"coin":"` + coinId + `","major":"` + casU(casGet(&casinoJackpot, jpKey(coinId, "major"))) +
244		`","grand":"` + casU(casGet(&casinoJackpot, jpKey(coinId, "grand"))) +
245		`","majorSeed":"` + casU(casGet(&casinoJpSeed, jpKey(coinId, "major"))) +
246		`","grandSeed":"` + casU(casGet(&casinoJpSeed, jpKey(coinId, "grand"))) +
247		`","refBet":"` + casU(jpRefBet(coinId)) + `","miniMult":` + casU(hsMiniMult) + `,"minorMult":` + casU(hsMinorMult) +
248		`,"majorBps":` + casU(jpMajorBps) + `,"grandBps":` + casU(jpGrandBps) + `}`
249}
250
251func validJpTier(tier string) bool { return tier == "major" || tier == "grand" }
252
253// SeedCasinoJackpot adds the ADMIN'S OWN coins to a jackpot pool (never withdrawable).
254func SeedCasinoJackpot(cur realm, coinId, tier, amount string) {
255	if cur.Previous().Address() != admin {
256		panic("unauthorized: admin only")
257	}
258	if _, ok := coinRarity[coinId]; !ok {
259		panic("unknown coin id: " + coinId)
260	}
261	if !validJpTier(tier) {
262		panic("tier must be major or grand")
263	}
264	amt, err := strconv.ParseUint(amount, 10, 64)
265	if err != nil || amt == 0 || amt > casinoMaxAmount {
266		panic("invalid amount")
267	}
268	casDebit(admin.String(), coinId, amt)
269	k := jpKey(coinId, tier)
270	casSet(&casinoJackpot, k, casAdd(casGet(&casinoJackpot, k), amt))
271	casStat(coinId, "", "seeded", amt)
272	chain.Emit("CasinoJackpotSeeded", "coin", coinId, "tier", tier, "amount", amount)
273}
274
275// SetCasinoJackpotSeed sets the level a jackpot is topped back up to after a hit.
276func SetCasinoJackpotSeed(cur realm, coinId, tier, amount string) {
277	if cur.Previous().Address() != admin {
278		panic("unauthorized: admin only")
279	}
280	if !validJpTier(tier) {
281		panic("tier must be major or grand")
282	}
283	amt, err := strconv.ParseUint(amount, 10, 64)
284	if err != nil || amt > casinoMaxAmount {
285		panic("invalid amount")
286	}
287	casSet(&casinoJpSeed, jpKey(coinId, tier), amt)
288}
289
290// SetCasinoJackpotRefBet sets the bet that wins a full jackpot (smaller bets win
291// the same share of the pool as their share of this bet).
292func SetCasinoJackpotRefBet(cur realm, coinId, amount string) {
293	if cur.Previous().Address() != admin {
294		panic("unauthorized: admin only")
295	}
296	amt, err := strconv.ParseUint(amount, 10, 64)
297	if err != nil || amt == 0 || amt > casinoMaxAmount {
298		panic("invalid amount")
299	}
300	casSet(&casinoJpRef, coinId, amt)
301}