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

12.56 Kb · 362 lines
  1package game31
  2
  3import (
  4	"crypto/sha256"
  5	"encoding/hex"
  6	"strconv"
  7	"strings"
  8	"time"
  9
 10	"chain"
 11)
 12
 13// ── rounds ──────────────────────────────────────────────────────────────────
 14
 15func getCasinoRound(id string) *casinoRound {
 16	if !casinoRounds.Has(id) {
 17		return nil
 18	}
 19	r, ok := casinoRounds.Get(id).(*casinoRound)
 20	if !ok {
 21		return nil
 22	}
 23	return r
 24}
 25
 26func mustCasinoRound(id string) *casinoRound {
 27	r := getCasinoRound(id)
 28	if r == nil {
 29		panic("round not found")
 30	}
 31	return r
 32}
 33
 34func casinoRoundBet(r *casinoRound, roundId string) *casinoBet {
 35	key := roundId + ":" + r.player
 36	if !casinoBets.Has(key) {
 37		return nil
 38	}
 39	return casinoBets.Get(key).(*casinoBet)
 40}
 41
 42// CommitCasinoSeed opens a round for one player with sha256(s). Operator only;
 43// a round id can be used once.
 44func CommitCasinoSeed(cur realm, roundId string, player string, seedHash string) {
 45	if !isOperator(cur) {
 46		panic("unauthorized: casino operator only")
 47	}
 48	if !casSafe(roundId, 64) {
 49		panic("invalid round id")
 50	}
 51	if casinoRounds.Has(roundId) {
 52		panic("round already exists")
 53	}
 54	if !address(player).IsValid() {
 55		panic("invalid player address")
 56	}
 57	seedHash = strings.ToLower(seedHash)
 58	if !casIsHex64(seedHash) {
 59		panic("seedHash must be sha256 hex")
 60	}
 61	casinoRounds.Set(roundId, &casinoRound{player: player, seedHash: seedHash, status: "committed", committedAt: time.Now().Unix()})
 62}
 63
 64// PlaceCasinoBet escrows the player's bet with sha256(c): only the round's
 65// player, only before s is revealed, and only if the pool can pay its largest win.
 66func PlaceCasinoBet(cur realm, roundId string, game string, coinId string, betAmount string, choice string, clientHash string) {
 67	requireNotPaused()
 68	caller := cur.Previous().Address().String()
 69	if isBlacklisted(caller) {
 70		panic("address banned")
 71	}
 72	r := mustCasinoRound(roundId)
 73	if r.player != caller {
 74		panic("this round belongs to another player")
 75	}
 76	if r.status != "committed" {
 77		panic("betting is closed for this round")
 78	}
 79	if time.Now().Unix()-r.committedAt >= casinoBetWindowSec {
 80		panic("round expired")
 81	}
 82	if _, ok := coinRarity[coinId]; !ok {
 83		panic("unknown coin id: " + coinId)
 84	}
 85	if !validCasinoChoice(game, choice) {
 86		panic("invalid game or choice: " + game + "/" + choice)
 87	}
 88	clientHash = strings.ToLower(clientHash)
 89	if !casIsHex64(clientHash) {
 90		panic("client hash must be sha256 hex")
 91	}
 92	amt, err := strconv.ParseUint(betAmount, 10, 64)
 93	if err != nil || amt == 0 || amt > casinoMaxAmount {
 94		panic("invalid bet amount")
 95	}
 96	if game == "slots" && getGrant(caller) != nil {
 97		panic("finish your free spins first")
 98	}
 99	// Check everything before any write, then debit and reserve the largest win.
100	casinoCheckBet(coinId, game, amt)
101	exp := casinoExposure(game, amt)
102	casDebit(caller, coinId, amt)
103	casinoReserve(coinId, exp)
104	casinoBets.Set(roundId+":"+caller, &casinoBet{addr: caller, game: game, coin: coinId, choice: choice, clientHash: clientHash, amount: amt, exposure: exp})
105	r.status = "bet"
106	r.betAt = time.Now().Unix()
107	r.bets = 1
108}
109
110// RevealCasinoSeed publishes s (it must hash to the commit). It settles nothing:
111// the bet settles when the player's secret is revealed.
112func RevealCasinoSeed(cur realm, roundId string, serverSeed string) {
113	if !isOperator(cur) {
114		panic("unauthorized: casino operator only")
115	}
116	r := mustCasinoRound(roundId)
117	if r.status != "committed" && r.status != "bet" {
118		panic("round already revealed or closed")
119	}
120	h := sha256.Sum256([]byte(serverSeed))
121	if hex.EncodeToString(h[:]) != r.seedHash {
122		panic("server seed does not match the committed hash")
123	}
124	r.serverSeed = serverSeed
125	if r.status == "committed" {
126		r.status = "closed" // no bet was placed: the round just ends
127		return
128	}
129	r.status = "seeded"
130	r.seededAt = time.Now().Unix()
131}
132
133// RevealCasinoSecret publishes the player's secret c (anyone may relay it: it
134// must hash to the bet's hash) and settles the bet on chain. Works while paused.
135func RevealCasinoSecret(cur realm, roundId string, clientSecret string) {
136	r := mustCasinoRound(roundId)
137	if r.status != "seeded" {
138		panic("round is not waiting for the client secret")
139	}
140	b := casinoRoundBet(r, roundId)
141	if b == nil || b.settled {
142		panic("no open bet in this round")
143	}
144	if !casSafe(clientSecret, 64) {
145		panic("invalid client secret")
146	}
147	if sha256Hex(clientSecret) != b.clientHash {
148		panic("client secret does not match the bet's hash")
149	}
150	b.clientSecret = clientSecret
151	casinoResolve(roundId, r, b)
152	r.status = "settled"
153}
154
155// casinoResolve rolls the bet from s and c, pays it, and records the result.
156func casinoResolve(roundId string, r *casinoRound, b *casinoBet) {
157	payout := uint64(0)
158	switch b.game {
159	case "slots":
160		s := newSlotsStream(r.serverSeed, b.clientSecret, roundId)
161		g := slotsSpinGrid(s)
162		mult, lines := slotsLineMult(&g)
163		sc := slotsCount(&g, symScatter)
164		win := casMul(b.amount/slotsBetStep, mult)
165		if sc >= 3 {
166			win = casAdd(win, casMul(b.amount, slotsScatterPay[sc]))
167		}
168		b.grid = slotsGridString(&g)
169		b.lineMult = mult
170		b.lines = lines
171		b.scatters = sc
172		if b.freeSpin {
173			// ×2, no retrigger, no Hold & Spin; paid from the grant's reservation.
174			payout = settleFreeSpin(b, casMul(win, freeSpinMult))
175			b.payout = payout
176			b.won = payout > 0
177			b.outcome = "lose"
178			if payout > 0 {
179				b.outcome = "win"
180			}
181			b.settled = true
182			casRecord(b.coin, "slots", 0, payout)
183			casStat(b.coin, "slots", "freeSpins", 1)
184			chain.Emit("CasinoBetSettled", "round", roundId, "player", b.addr, "game", "slots", "coin", b.coin,
185				"amount", "0", "freeSpin", "true", "outcome", b.outcome, "won", strconv.FormatBool(b.won), "payout", casU(payout))
186			return
187		}
188		major, grand := false, false
189		if slotsCount(&g, symVault) >= 3 {
190			tiles, played, full := slotsHoldAndSpin(s, &g)
191			hm, mj, gr := slotsHSPayout(&tiles, full)
192			b.hsTiles = hsTilesString(&tiles)
193			b.hsRespins = played
194			b.hsMult = hm
195			major, grand = mj, gr
196			win = casAdd(win, casMul(b.amount, hm))
197		}
198		capWin := casMul(b.amount, slotsMaxWinMult)
199		if win > capWin {
200			win = capWin
201			b.capped = true
202		}
203		payout = casinoSettle(b.addr, b.coin, b.amount, b.exposure, win)
204		if sc >= 3 {
205			// The rest of this spin's 500× cap stays reserved for its free spins
206			// (reserved before anything else leaves the pool, so pool ≥ reserved holds).
207			n := slotsFreeSpins[sc]
208			capLeft := capWin - win
209			casinoReserve(b.coin, capLeft)
210			casinoGrants.Set(b.addr, &freeSpinGrant{addr: b.addr, coin: b.coin, bet: b.amount, left: n, total: n, capLeft: capLeft, fromRound: roundId, grantedAt: time.Now().Unix()})
211			b.fsGranted = n
212			chain.Emit("CasinoFreeSpinsGranted", "player", b.addr, "coin", b.coin, "spins", casU(n), "bet", casU(b.amount))
213		}
214		casinoJackpotContribute(b.coin, b.amount)
215		jw := uint64(0)
216		if grand {
217			jw = casAdd(jw, casinoJackpotPay(b.addr, b.coin, "grand", b.amount))
218			b.jackpots = "grand"
219		}
220		if major {
221			jw = casAdd(jw, casinoJackpotPay(b.addr, b.coin, "major", b.amount))
222			if b.jackpots != "" {
223				b.jackpots = "major+" + b.jackpots
224			} else {
225				b.jackpots = "major"
226			}
227		}
228		b.jackpotWin = jw
229		b.payout = casAdd(payout, jw)
230		b.won = b.payout > b.amount
231		b.outcome = "lose"
232		if b.payout > 0 {
233			b.outcome = "win"
234		}
235		b.settled = true
236		casRecord(b.coin, "slots", b.amount, b.payout)
237		chain.Emit("CasinoBetSettled", "round", roundId, "player", b.addr, "game", "slots", "coin", b.coin,
238			"amount", casU(b.amount), "outcome", b.outcome, "won", strconv.FormatBool(b.won), "payout", casU(b.payout),
239			"jackpots", b.jackpots, "freeSpins", casU(b.fsGranted))
240		return
241	case "rps":
242		b.roll = CasinoRoll(r.serverSeed, b.clientSecret, roundId, b.game)
243		res := CasinoOutcome("rps", b.roll)
244		b.outcome = rpsHouseMove(b.choice, res)
245		switch res {
246		case "win":
247			payout = casAdd(b.amount, b.amount)
248			b.won = true
249		case "push":
250			payout = b.amount
251		}
252	default:
253		b.roll = CasinoRoll(r.serverSeed, b.clientSecret, roundId, b.game)
254		b.outcome = CasinoOutcome(b.game, b.roll)
255		if b.outcome == b.choice {
256			payout = casAdd(b.amount, b.amount)
257			b.won = true
258		}
259	}
260	b.payout = casinoSettle(b.addr, b.coin, b.amount, b.exposure, payout)
261	b.settled = true
262	casRecord(b.coin, b.game, b.amount, b.payout)
263	chain.Emit("CasinoBetSettled", "round", roundId, "player", b.addr, "game", b.game, "coin", b.coin,
264		"amount", casU(b.amount), "roll", casU(b.roll), "outcome", b.outcome, "won", strconv.FormatBool(b.won), "payout", casU(b.payout))
265}
266
267// RefundCasinoBet returns the stake only if s was never revealed and 30 minutes
268// have passed since the bet. Never after s is revealed. Works while paused.
269func RefundCasinoBet(cur realm, roundId string) {
270	caller := cur.Previous().Address().String()
271	r := mustCasinoRound(roundId)
272	if r.status == "refunded" {
273		panic("bet already settled")
274	}
275	if r.status != "bet" {
276		panic("round already revealed: no refunds")
277	}
278	if r.player != caller {
279		panic("no bet found for this round")
280	}
281	if time.Now().Unix()-r.betAt < casinoSeedTimeoutSec {
282		panic("refund opens 30 minutes after the bet if the server seed is never revealed")
283	}
284	b := casinoRoundBet(r, roundId)
285	b.settled = true
286	b.refunded = true
287	if b.freeSpin {
288		// No stake was taken: the free spin goes back to the grant.
289		if g := getGrant(caller); g != nil && g.inRound == roundId {
290			g.inRound = ""
291		}
292	} else {
293		casSet(&casinoReserved, b.coin, casSub(casGet(&casinoReserved, b.coin), b.exposure))
294		casCredit(caller, b.coin, b.amount)
295	}
296	r.status = "refunded"
297	chain.Emit("CasinoBetRefunded", "round", roundId, "player", caller, "amount", casU(b.amount))
298}
299
300// ForfeitCasinoBet settles a bet as a loss when s was revealed but the player's
301// secret was not within 24 h (only the player could know the result). Anyone.
302func ForfeitCasinoBet(cur realm, roundId string) {
303	r := mustCasinoRound(roundId)
304	if r.status != "seeded" {
305		panic("round is not waiting for the client secret")
306	}
307	if time.Now().Unix()-r.seededAt < casinoSecretTimeoutSec {
308		panic("forfeit opens 24 hours after the server seed is revealed")
309	}
310	b := casinoRoundBet(r, roundId)
311	b.forfeited = true
312	b.outcome = "forfeit"
313	if b.freeSpin {
314		b.payout = settleFreeSpin(b, 0) // the free spin is used, it wins nothing
315		casRecord(b.coin, b.game, 0, 0)
316	} else {
317		b.payout = casinoSettle(b.addr, b.coin, b.amount, b.exposure, 0)
318		casRecord(b.coin, b.game, b.amount, 0)
319	}
320	b.settled = true
321	r.status = "settled"
322	chain.Emit("CasinoBetForfeited", "round", roundId, "player", b.addr, "amount", casU(b.amount))
323}
324
325// ── queries ─────────────────────────────────────────────────────────────────
326
327func CasinoRound(roundId string) string {
328	r := getCasinoRound(roundId)
329	if r == nil {
330		return "null"
331	}
332	return `{"roundId":"` + roundId + `","player":"` + r.player + `","seedHash":"` + r.seedHash + `","serverSeed":"` + r.serverSeed +
333		`","status":"` + r.status + `","committedAt":` + strconv.FormatInt(r.committedAt, 10) +
334		`,"betAt":` + strconv.FormatInt(r.betAt, 10) + `,"seededAt":` + strconv.FormatInt(r.seededAt, 10) +
335		`,"bets":` + strconv.Itoa(r.bets) + `}`
336}
337
338// CasinoBet returns one player's bet and its on-chain result.
339func CasinoBet(roundId string, addr string) string {
340	key := roundId + ":" + addr
341	if !casinoBets.Has(key) {
342		return "null"
343	}
344	b := casinoBets.Get(key).(*casinoBet)
345	return `{"addr":"` + b.addr + `","game":"` + b.game + `","coinId":"` + b.coin + `","amount":` + casU(b.amount) +
346		`,"exposure":` + casU(b.exposure) + `,"choice":"` + b.choice + `","clientHash":"` + b.clientHash +
347		`","clientSecret":"` + b.clientSecret + `","clientSeed":"` + b.clientSecret + `","settled":` + strconv.FormatBool(b.settled) +
348		`,"refunded":` + strconv.FormatBool(b.refunded) + `,"forfeited":` + strconv.FormatBool(b.forfeited) +
349		`,"won":` + strconv.FormatBool(b.won) + `,"roll":` + casU(b.roll) + `,"outcome":"` + b.outcome +
350		`","payout":` + casU(b.payout) + `,"grid":"` + b.grid + `","lineMult":` + casU(b.lineMult) +
351		`,"lines":` + strconv.Itoa(b.lines) + `,"scatters":` + strconv.Itoa(b.scatters) +
352		`,"freeSpin":` + strconv.FormatBool(b.freeSpin) + `,"capped":` + strconv.FormatBool(b.capped) +
353		`,"fsGranted":` + casU(b.fsGranted) + `,"hsTiles":"` + b.hsTiles + `","hsRespins":` + strconv.Itoa(b.hsRespins) +
354		`,"hsMult":` + casU(b.hsMult) + `,"jackpots":"` + b.jackpots + `","jackpotWin":` + casU(b.jackpotWin) + `}`
355}
356
357// JackpotPool reports the GRAND jackpot pool (the casino bankroll is CasinoPool).
358func JackpotPool(coinId string) string {
359	return `{"pool":"` + casU(casGet(&casinoJackpot, jpKey(coinId, "grand"))) + `","coinId":"` + coinId + `"}`
360}
361
362func CasinoHistory(_ string) string { return "[]" }