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

15.05 Kb · 493 lines
  1// GnoMoji — Main Game Realm
  2// gno.land/r/gnomoji/game
  3//
  4// On-chain game engine for GnoMoji on Gno.land (v1.5.0 / Onyx testnet API).
  5// Tracks mined coins, casino rounds, PvP lobbies, tools, skills, achievements.
  6//
  7// Token minting is handled by gno.land/r/gnomoji/minter (the treasury realm).
  8// GRC20 coin realms live at gno.land/r/gnomoji/coins/{coinId}.
  9
 10package game
 11
 12import (
 13	"strconv"
 14	"strings"
 15	"time"
 16
 17	"gno.land/p/nt/avl/v0"
 18	"gno.land/p/nt/ufmt/v0"
 19)
 20
 21// ── State ────────────────────────────────────────────────────────────────────
 22
 23var (
 24	admin          address
 25	pendingMints   avl.Tree // addr+":"+coinId → amount string
 26	balances       avl.Tree // addr+":"+coinId → amount string
 27	veinSupply     avl.Tree // coinId → "remaining:max"
 28	casinoRounds   avl.Tree // roundId → JSON string
 29	pvpLobbies     avl.Tree // lobbyId → JSON string
 30	ownedTools     avl.Tree // addr → comma-separated tool IDs
 31	toolLife       avl.Tree // addr+":"+toolId → remaining life string
 32	lifetimeMined  avl.Tree // addr+":"+coinId → total mined string
 33	achievements   avl.Tree // addr+":"+achId → "1"
 34	dailyCheckins  avl.Tree // addr → unix seconds string
 35	skillRanks     avl.Tree // addr+":"+skillId → rank string
 36	miners         avl.Tree // addr → total mined all-time string
 37	paused         bool
 38)
 39
 40func init(cur realm) {
 41	admin = cur.Address()
 42}
 43
 44// ── Admin ────────────────────────────────────────────────────────────────────
 45
 46func isAdmin(cur realm) bool {
 47	return cur.Previous().Address() == admin
 48}
 49
 50func UpdateConfig(cur realm, newAdmin string) {
 51	if !isAdmin(cur) {
 52		panic("unauthorized")
 53	}
 54	admin = address(newAdmin)
 55}
 56
 57func SetPaused(cur realm, p bool) {
 58	if !isAdmin(cur) {
 59		panic("unauthorized")
 60	}
 61	paused = p
 62}
 63
 64func requireNotPaused() {
 65	if paused {
 66		panic("game is paused")
 67	}
 68}
 69
 70// ── Mining ───────────────────────────────────────────────────────────────────
 71
 72// ClaimMine records mined coins into pending mints for the caller.
 73// coinsJson format: [["coinId","amount"],...]
 74func ClaimMine(cur realm, coinsJson string) {
 75	requireNotPaused()
 76	caller := cur.Previous().Address().String()
 77
 78	coinsJson = strings.TrimSpace(coinsJson)
 79	coinsJson = strings.TrimPrefix(coinsJson, "[")
 80	coinsJson = strings.TrimSuffix(coinsJson, "]")
 81
 82	for _, entry := range splitJsonArray(coinsJson) {
 83		entry = strings.TrimSpace(entry)
 84		entry = strings.TrimPrefix(entry, "[")
 85		entry = strings.TrimSuffix(entry, "]")
 86		parts := strings.SplitN(entry, ",", 2)
 87		if len(parts) != 2 {
 88			continue
 89		}
 90		coinId := strings.Trim(strings.TrimSpace(parts[0]), `"`)
 91		amtStr := strings.Trim(strings.TrimSpace(parts[1]), `"`)
 92		amt, err := strconv.ParseUint(amtStr, 10, 64)
 93		if err != nil || amt == 0 {
 94			continue
 95		}
 96
 97		// Cap at vein supply if registered
 98		if veinSupply.Has(coinId) {
 99			vsStr := veinSupply.Get(coinId).(string)
100			p2 := strings.SplitN(vsStr, ":", 2)
101			if len(p2) == 2 {
102				rem, _ := strconv.ParseUint(p2[0], 10, 64)
103				if rem == 0 {
104					continue
105				}
106				if amt > rem {
107					amt = rem
108				}
109				maxV, _ := strconv.ParseUint(p2[1], 10, 64)
110				veinSupply.Set(coinId, ufmt.Sprintf("%d:%d", rem-amt, maxV))
111			}
112		}
113
114		key := caller + ":" + coinId
115
116		// Accumulate pending mints
117		ex := uint64(0)
118		if pendingMints.Has(key) {
119			ex, _ = strconv.ParseUint(pendingMints.Get(key).(string), 10, 64)
120		}
121		pendingMints.Set(key, strconv.FormatUint(ex+amt, 10))
122
123		// Track lifetime mined per coin
124		lifEx := uint64(0)
125		if lifetimeMined.Has(key) {
126			lifEx, _ = strconv.ParseUint(lifetimeMined.Get(key).(string), 10, 64)
127		}
128		lifetimeMined.Set(key, strconv.FormatUint(lifEx+amt, 10))
129
130		// Update leaderboard total (all coins summed)
131		mt := uint64(0)
132		if miners.Has(caller) {
133			mt, _ = strconv.ParseUint(miners.Get(caller).(string), 10, 64)
134		}
135		miners.Set(caller, strconv.FormatUint(mt+amt, 10))
136	}
137}
138
139// ClearPendingMints is called by the minter realm after successful GRC20 minting.
140func ClearPendingMints(cur realm, playerAddr string, coinsJson string) {
141	if !isAdmin(cur) {
142		panic("unauthorized: only admin/minter can clear pending mints")
143	}
144	coinsJson = strings.TrimSpace(coinsJson)
145	coinsJson = strings.TrimPrefix(coinsJson, "[")
146	coinsJson = strings.TrimSuffix(coinsJson, "]")
147	for _, entry := range splitJsonArray(coinsJson) {
148		coinId := strings.Trim(strings.TrimSpace(entry), `"`)
149		if coinId != "" {
150			pendingMints.Remove(playerAddr + ":" + coinId)
151		}
152	}
153}
154
155// ── Casino ───────────────────────────────────────────────────────────────────
156
157func CommitCasinoSeed(cur realm, roundId string, seedHash string) {
158	if !isAdmin(cur) {
159		panic("unauthorized")
160	}
161	casinoRounds.Set(roundId, `{"roundId":"`+roundId+`","seedHash":"`+seedHash+`","status":"committed","bets":[]}`)
162}
163
164func PlaceCasinoBet(cur realm, roundId string, game string, coinId string, betAmount string, choice string, clientSeed string) {
165	requireNotPaused()
166	caller := cur.Previous().Address().String()
167	amt, err := strconv.ParseUint(betAmount, 10, 64)
168	if err != nil || amt == 0 {
169		panic("invalid bet amount")
170	}
171
172	balKey := caller + ":" + coinId
173	existing := uint64(0)
174	if balances.Has(balKey) {
175		existing, _ = strconv.ParseUint(balances.Get(balKey).(string), 10, 64)
176	}
177	if existing < amt {
178		panic("insufficient balance")
179	}
180	balances.Set(balKey, strconv.FormatUint(existing-amt, 10))
181
182	if !casinoRounds.Has(roundId) {
183		panic("round not found")
184	}
185	bet := `{"addr":"` + caller + `","game":"` + game + `","coinId":"` + coinId + `","amount":` + betAmount + `,"choice":"` + choice + `","clientSeed":"` + clientSeed + `"}`
186	casinoRounds.Set(roundId+":bet:"+caller, bet)
187}
188
189func RevealCasinoSeed(cur realm, roundId string, serverSeed string) {
190	if !isAdmin(cur) {
191		panic("unauthorized")
192	}
193	casinoRounds.Set(roundId+":serverSeed", serverSeed)
194	casinoRounds.Set(roundId+":status", "revealed")
195}
196
197func RefundCasinoBet(cur realm, roundId string) {
198	requireNotPaused()
199	caller := cur.Previous().Address().String()
200	betKey := roundId + ":bet:" + caller
201	if !casinoRounds.Has(betKey) {
202		panic("no bet found for this round")
203	}
204	casinoRounds.Remove(betKey)
205}
206
207// ── PvP Lobbies ──────────────────────────────────────────────────────────────
208
209func CreatePvpLobby(cur realm, game string, coinId string, betAmount string) string {
210	requireNotPaused()
211	caller := cur.Previous().Address().String()
212	prefix := caller
213	if len(prefix) > 8 {
214		prefix = caller[:8]
215	}
216	lobbyId := ufmt.Sprintf("pvp-%s-%d", prefix, time.Now().Unix())
217	lobby := `{"lobbyId":"` + lobbyId + `","creator":"` + caller + `","game":"` + game + `","coinId":"` + coinId + `","betAmount":` + betAmount + `,"status":"open","round":1,"createdAt":` + strconv.FormatInt(time.Now().Unix(), 10) + `}`
218	pvpLobbies.Set(lobbyId, lobby)
219	return lobbyId
220}
221
222func JoinPvpLobby(cur realm, lobbyId string) {
223	requireNotPaused()
224	caller := cur.Previous().Address().String()
225	if !pvpLobbies.Has(lobbyId) {
226		panic("lobby not found")
227	}
228	pvpLobbies.Set(lobbyId+":challenger", caller)
229	pvpLobbies.Set(lobbyId+":status", "joined")
230}
231
232func CommitPvpMove(cur realm, lobbyId string, moveHash string) {
233	requireNotPaused()
234	caller := cur.Previous().Address().String()
235	pvpLobbies.Set(lobbyId+":commit:"+caller, moveHash)
236}
237
238func RevealPvpMove(cur realm, lobbyId string, choice string, salt string) {
239	requireNotPaused()
240	caller := cur.Previous().Address().String()
241	pvpLobbies.Set(lobbyId+":reveal:"+caller, choice+":"+salt)
242	pvpLobbies.Set(lobbyId+":status", "revealed")
243}
244
245func ClaimPvpTimeout(cur realm, lobbyId string) {
246	requireNotPaused()
247	caller := cur.Previous().Address().String()
248	pvpLobbies.Set(lobbyId+":winner", caller)
249	pvpLobbies.Set(lobbyId+":status", "finished")
250}
251
252func ExpireLobbies(cur realm) {
253	if !isAdmin(cur) {
254		panic("unauthorized")
255	}
256}
257
258// ── Tools ────────────────────────────────────────────────────────────────────
259
260func BuyTool(cur realm, toolId string) {
261	requireNotPaused()
262	caller := cur.Previous().Address().String()
263	existing := ""
264	if ownedTools.Has(caller) {
265		existing = ownedTools.Get(caller).(string)
266	}
267	if existing == "" {
268		ownedTools.Set(caller, toolId)
269	} else {
270		ownedTools.Set(caller, existing+","+toolId)
271	}
272}
273
274func RepairTool(cur realm, toolId string) {
275	requireNotPaused()
276	caller := cur.Previous().Address().String()
277	toolLife.Set(caller+":"+toolId, "10000")
278}
279
280// ── Progression ──────────────────────────────────────────────────────────────
281
282func TrainSkill(cur realm, skillId string) {
283	requireNotPaused()
284	caller := cur.Previous().Address().String()
285	key := caller + ":" + skillId
286	rank := uint64(0)
287	if skillRanks.Has(key) {
288		rank, _ = strconv.ParseUint(skillRanks.Get(key).(string), 10, 64)
289	}
290	skillRanks.Set(key, strconv.FormatUint(rank+1, 10))
291}
292
293func ClaimAchievement(cur realm, achievementId string) {
294	requireNotPaused()
295	caller := cur.Previous().Address().String()
296	key := caller + ":" + achievementId
297	if achievements.Has(key) {
298		panic("already claimed")
299	}
300	achievements.Set(key, "1")
301}
302
303func DailyCheckin(cur realm) {
304	requireNotPaused()
305	caller := cur.Previous().Address().String()
306	now := time.Now().Unix()
307	if dailyCheckins.Has(caller) {
308		last, _ := strconv.ParseInt(dailyCheckins.Get(caller).(string), 10, 64)
309		if now-last < 72000 {
310			panic("too soon for daily check-in")
311		}
312	}
313	dailyCheckins.Set(caller, strconv.FormatInt(now, 10))
314}
315
316func CompleteTask(cur realm, taskId string) {
317	requireNotPaused()
318	caller := cur.Previous().Address().String()
319	achievements.Set(caller+":"+taskId, "task:1")
320}
321
322func RegisterVein(cur realm, coinId string, maxSupply string) {
323	if !isAdmin(cur) {
324		panic("unauthorized")
325	}
326	veinSupply.Set(coinId, maxSupply+":"+maxSupply)
327}
328
329// ── Queries (pure reads — no cur realm needed) ────────────────────────────────
330
331func PendingMints(playerAddr string) string {
332	var parts []string
333	pendingMints.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
334		coinId := strings.TrimPrefix(key, playerAddr+":")
335		parts = append(parts, `["`+coinId+`","`+val.(string)+`"]`)
336		return false
337	})
338	return ufmt.Sprintf(`{"pending":[%s]}`, strings.Join(parts, ","))
339}
340
341func AllBalances(playerAddr string) string {
342	var parts []string
343	balances.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
344		coinId := strings.TrimPrefix(key, playerAddr+":")
345		parts = append(parts, `"`+coinId+`":`+val.(string))
346		return false
347	})
348	return "{" + strings.Join(parts, ",") + "}"
349}
350
351func VeinSupply(coinId string) string {
352	if !veinSupply.Has(coinId) {
353		return `{"remaining":"0","max":"0"}`
354	}
355	p := strings.SplitN(veinSupply.Get(coinId).(string), ":", 2)
356	if len(p) != 2 {
357		return `{"remaining":"0","max":"0"}`
358	}
359	return `{"remaining":"` + p[0] + `","max":"` + p[1] + `"}`
360}
361
362func CasinoRound(roundId string) string {
363	if !casinoRounds.Has(roundId) {
364		return "null"
365	}
366	return casinoRounds.Get(roundId).(string)
367}
368
369func CasinoHistory(_ string) string { return "[]" }
370
371func PvpLobby(lobbyId string) string {
372	if !pvpLobbies.Has(lobbyId) {
373		return "null"
374	}
375	return pvpLobbies.Get(lobbyId).(string)
376}
377
378func OpenLobbies() string {
379	var parts []string
380	pvpLobbies.Iterate("pvp-", "pvp-\xFF", func(key string, val any) bool {
381		if !strings.Contains(key, ":") {
382			parts = append(parts, val.(string))
383		}
384		return false
385	})
386	return "[" + strings.Join(parts, ",") + "]"
387}
388
389func JackpotPool(coinId string) string {
390	return `{"pool":"0","coinId":"` + coinId + `"}`
391}
392
393func PlayerProfile(playerAddr string) string {
394	return `{"address":"` + playerAddr + `"}`
395}
396
397func OwnedTools(playerAddr string) string {
398	if !ownedTools.Has(playerAddr) {
399		return `["hands"]`
400	}
401	ids := strings.Split(ownedTools.Get(playerAddr).(string), ",")
402	var quoted []string
403	for _, id := range ids {
404		quoted = append(quoted, `"`+id+`"`)
405	}
406	return "[" + strings.Join(quoted, ",") + "]"
407}
408
409func LifetimeMined(playerAddr string) string {
410	var parts []string
411	lifetimeMined.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
412		coinId := strings.TrimPrefix(key, playerAddr+":")
413		parts = append(parts, `"`+coinId+`":`+val.(string))
414		return false
415	})
416	return "{" + strings.Join(parts, ",") + "}"
417}
418
419func AllMiners(limit int, offset int) string {
420	var parts []string
421	count, skipped := 0, 0
422	miners.Iterate("", "\xFF", func(key string, val any) bool {
423		if skipped < offset {
424			skipped++
425			return false
426		}
427		if count >= limit {
428			return true
429		}
430		display := key
431		if len(display) > 12 {
432			display = display[:8] + "..."
433		}
434		parts = append(parts, `{"address":"`+key+`","display":"`+display+`","total":`+val.(string)+`}`)
435		count++
436		return false
437	})
438	return "[" + strings.Join(parts, ",") + "]"
439}
440
441func GetMedals(playerAddr string) string {
442	var claimed []string
443	achievements.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
444		achId := strings.TrimPrefix(key, playerAddr+":")
445		claimed = append(claimed, `"`+achId+`"`)
446		return false
447	})
448	return ufmt.Sprintf(`{"medals":[%s]}`, strings.Join(claimed, ","))
449}
450
451func DailyCheckinStatus(playerAddr string) string {
452	now := time.Now().Unix()
453	last := int64(0)
454	if dailyCheckins.Has(playerAddr) {
455		last, _ = strconv.ParseInt(dailyCheckins.Get(playerAddr).(string), 10, 64)
456	}
457	done := now-last < 72000
458	return ufmt.Sprintf(`{"ok":true,"done":%t,"streak":1,"next":%d}`, done, last+86400)
459}
460
461func ListTasks(_ string) string { return `{"ok":true,"tasks":[]}` }
462
463// ── Render ───────────────────────────────────────────────────────────────────
464
465func Render(_ string) string {
466	return ufmt.Sprintf("# GnoMoji Game Realm\n\nAdmin: `%s`\n\nPath: `gno.land/r/g138gdmwj6heduu3st796lw6qqdwylgaxf7ty5gz/game`\n\nMine emoji coins on Gno.land!\n", admin.String())
467}
468
469// ── Helpers ──────────────────────────────────────────────────────────────────
470
471func splitJsonArray(s string) []string {
472	var result []string
473	depth, start := 0, 0
474	for i := 0; i < len(s); i++ {
475		switch s[i] {
476		case '[', '{':
477			depth++
478		case ']', '}':
479			depth--
480		case ',':
481			if depth == 0 {
482				if e := strings.TrimSpace(s[start:i]); e != "" {
483					result = append(result, e)
484				}
485				start = i + 1
486			}
487		}
488	}
489	if e := strings.TrimSpace(s[start:]); e != "" {
490		result = append(result, e)
491	}
492	return result
493}