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

12.95 Kb · 456 lines
  1package game27
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"time"
  7
  8	"gno.land/p/nt/ufmt/v0"
  9)
 10
 11
 12// ── Coin Transfer ────────────────────────────────────────────────────────────
 13
 14// Transfer sends coinId coins from the caller to toAddr directly on-chain.
 15// Both addresses must have interacted with the game realm at least once.
 16func Transfer(cur realm, coinId string, toAddr string, amountStr string) {
 17	requireNotPaused()
 18	caller := cur.Previous().Address().String()
 19	if isBlacklisted(caller) {
 20		panic("address banned")
 21	}
 22	if !strings.HasPrefix(toAddr, "g1") {
 23		panic("invalid destination address")
 24	}
 25	if caller == toAddr {
 26		panic("cannot transfer to yourself")
 27	}
 28	amount, err := strconv.ParseUint(amountStr, 10, 64)
 29	if err != nil || amount == 0 {
 30		panic("invalid amount")
 31	}
 32
 33	fromKey := caller + ":" + coinId
 34	have := uint64(0)
 35	if balances.Has(fromKey) {
 36		have, _ = strconv.ParseUint(balances.Get(fromKey).(string), 10, 64)
 37	}
 38	if have < amount {
 39		panic("insufficient balance: need " + amountStr + " have " + strconv.FormatUint(have, 10))
 40	}
 41
 42	// Deduct from sender
 43	balances.Set(fromKey, strconv.FormatUint(have-amount, 10))
 44
 45	// Credit recipient
 46	toKey := toAddr + ":" + coinId
 47	toHave := uint64(0)
 48	if balances.Has(toKey) {
 49		toHave, _ = strconv.ParseUint(balances.Get(toKey).(string), 10, 64)
 50	}
 51	balances.Set(toKey, strconv.FormatUint(toHave+amount, 10))
 52}
 53
 54// ── Tools ────────────────────────────────────────────────────────────────────
 55
 56func BuyTool(cur realm, toolId string) {
 57	requireNotPaused()
 58	caller := cur.Previous().Address().String()
 59	if isBlacklisted(caller) {
 60		panic("address banned")
 61	}
 62
 63	// Look up cost
 64	costs, hasCost := toolCosts[toolId]
 65	if !hasCost && toolId != "hands" {
 66		panic("unknown tool: " + toolId)
 67	}
 68
 69	// Deduct balance (hands is free, others require coins)
 70	if hasCost && len(costs) > 0 {
 71		deductBalances(caller, costs)
 72	}
 73
 74	// Grant tool
 75	existing := ""
 76	if ownedTools.Has(caller) {
 77		existing = ownedTools.Get(caller).(string)
 78	}
 79	if existing == "" {
 80		ownedTools.Set(caller, toolId)
 81	} else {
 82		ownedTools.Set(caller, existing+","+toolId)
 83	}
 84}
 85
 86func RepairTool(cur realm, toolId string) {
 87	requireNotPaused()
 88	caller := cur.Previous().Address().String()
 89	if isBlacklisted(caller) {
 90		panic("address banned")
 91	}
 92
 93	maxLife, hasLife := toolMaxLife[toolId]
 94	if !hasLife || maxLife == 0 {
 95		panic("tool cannot be repaired: " + toolId)
 96	}
 97
 98	// Cost = 30% of craft cost (rounded up per coin)
 99	baseCosts, hasCost := toolCosts[toolId]
100	if !hasCost {
101		panic("unknown tool: " + toolId)
102	}
103	var repairCosts [][2]string
104	for _, pair := range baseCosts {
105		coinId, amtStr := pair[0], pair[1]
106		baseAmt, _ := strconv.ParseUint(amtStr, 10, 64)
107		// 30% rounded up: (base * 30 + 99) / 100
108		repairAmt := (baseAmt*30 + 99) / 100
109		if repairAmt > 0 {
110			repairCosts = append(repairCosts, [2]string{coinId, strconv.FormatUint(repairAmt, 10)})
111		}
112	}
113
114	deductBalances(caller, repairCosts)
115
116	// Restore full durability
117	toolLife.Set(caller+":"+toolId, strconv.FormatUint(maxLife, 10))
118}
119
120// ── Progression ──────────────────────────────────────────────────────────────
121
122func TrainSkill(cur realm, skillId string) {
123	requireNotPaused()
124	caller := cur.Previous().Address().String()
125	if isBlacklisted(caller) {
126		panic("address banned")
127	}
128	key := caller + ":" + skillId
129
130	// Look up current rank
131	rank := uint64(0)
132	if skillRanks.Has(key) {
133		rank, _ = strconv.ParseUint(skillRanks.Get(key).(string), 10, 64)
134	}
135
136	// Check rank cap
137	maxRank, hasMax := skillMaxRanks[skillId]
138	if !hasMax {
139		panic("unknown skill: " + skillId)
140	}
141	if rank >= maxRank {
142		panic("skill already at maximum rank")
143	}
144
145	// Compute cost: base × 2^rank
146	baseCosts, hasCost := skillBaseCosts[skillId]
147	if !hasCost {
148		panic("unknown skill: " + skillId)
149	}
150	var scaledCosts [][2]string
151	multiplier := uint64(1)
152	for i := uint64(0); i < rank; i++ {
153		multiplier *= 2
154	}
155	for _, pair := range baseCosts {
156		coinId, amtStr := pair[0], pair[1]
157		baseAmt, _ := strconv.ParseUint(amtStr, 10, 64)
158		scaled := baseAmt * multiplier
159		if scaled > 0 {
160			scaledCosts = append(scaledCosts, [2]string{coinId, strconv.FormatUint(scaled, 10)})
161		}
162	}
163
164	deductBalances(caller, scaledCosts)
165
166	// Increment rank
167	skillRanks.Set(key, strconv.FormatUint(rank+1, 10))
168}
169
170func ClaimAchievement(cur realm, achievementId string) {
171	requireNotPaused()
172	caller := cur.Previous().Address().String()
173	key := caller + ":" + achievementId
174	if achievements.Has(key) {
175		panic("already claimed")
176	}
177	achievements.Set(key, "1")
178}
179
180// BatchClaimAchievements claims multiple achievements in a single transaction.
181// idsJson format: ["ach1","ach2",...] — already-claimed IDs are silently skipped.
182// One tx instead of N txs — 10× cheaper for Claim All.
183func BatchClaimAchievements(cur realm, idsJson string) {
184	requireNotPaused()
185	caller := cur.Previous().Address().String()
186
187	idsJson = strings.TrimSpace(idsJson)
188	idsJson = strings.TrimPrefix(idsJson, "[")
189	idsJson = strings.TrimSuffix(idsJson, "]")
190
191	for _, part := range strings.Split(idsJson, ",") {
192		id := strings.Trim(strings.TrimSpace(part), "\"")
193		if id == "" {
194			continue
195		}
196		key := caller + ":" + id
197		if achievements.Has(key) {
198			continue
199		}
200		achievements.Set(key, "1")
201	}
202}
203
204func DailyCheckin(cur realm) {
205	requireNotPaused()
206	caller := cur.Previous().Address().String()
207	now := time.Now().Unix()
208	if dailyCheckins.Has(caller) {
209		last, _ := strconv.ParseInt(dailyCheckins.Get(caller).(string), 10, 64)
210		if now-last < 86400 {
211			panic("too soon for daily check-in")
212		}
213	}
214	dailyCheckins.Set(caller, strconv.FormatInt(now, 10))
215}
216
217// CreateTask registers a new task on-chain.
218func CreateTask(cur realm, taskId string, desc string) {
219	requireNotPaused()
220	caller := cur.Previous().Address().String()
221	// Store task metadata keyed by taskId; only the creator can create it
222	achievements.Set("task:"+taskId+":creator", caller)
223	achievements.Set("task:"+taskId+":desc", desc)
224}
225
226func CompleteTask(cur realm, taskId string) {
227	requireNotPaused()
228	caller := cur.Previous().Address().String()
229	achievements.Set(caller+":"+taskId, "task:1")
230}
231
232func RegisterVein(cur realm, coinId string, maxSupply string) {
233	if !isAdmin(cur) {
234		panic("unauthorized")
235	}
236	veinSupply.Set(coinId, maxSupply+":"+maxSupply)
237}
238
239// ── Queries (pure reads — no cur realm needed) ────────────────────────────────
240
241func PendingMints(playerAddr string) string {
242	var parts []string
243	pendingMints.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
244		coinId := strings.TrimPrefix(key, playerAddr+":")
245		parts = append(parts, `["`+coinId+`","`+val.(string)+`"]`)
246		return false
247	})
248	return ufmt.Sprintf(`{"pending":[%s]}`, strings.Join(parts, ","))
249}
250
251func AllBalances(playerAddr string) string {
252	var parts []string
253	balances.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
254		coinId := strings.TrimPrefix(key, playerAddr+":")
255		parts = append(parts, `"`+coinId+`":`+val.(string))
256		return false
257	})
258	return "{" + strings.Join(parts, ",") + "}"
259}
260
261func VeinSupply(coinId string) string {
262	if !veinSupply.Has(coinId) {
263		return `{"remaining":"0","max":"0"}`
264	}
265	p := strings.SplitN(veinSupply.Get(coinId).(string), ":", 2)
266	if len(p) != 2 {
267		return `{"remaining":"0","max":"0"}`
268	}
269	return `{"remaining":"` + p[0] + `","max":"` + p[1] + `"}`
270}
271
272func CasinoRound(roundId string) string {
273	if !casinoRounds.Has(roundId) {
274		return "null"
275	}
276	return casinoRounds.Get(roundId).(string)
277}
278
279func CasinoHistory(_ string) string { return "[]" }
280
281func PvpLobby(lobbyId string) string {
282	if !pvpLobbies.Has(lobbyId) {
283		return "null"
284	}
285	return pvpLobbies.Get(lobbyId).(string)
286}
287
288func OpenLobbies() string {
289	var parts []string
290	pvpLobbies.Iterate("pvp-", "pvp-\xFF", func(key string, val any) bool {
291		if !strings.Contains(key, ":") {
292			parts = append(parts, val.(string))
293		}
294		return false
295	})
296	return "[" + strings.Join(parts, ",") + "]"
297}
298
299func JackpotPool(coinId string) string {
300	return `{"pool":"0","coinId":"` + coinId + `"}`
301}
302
303func PlayerProfile(playerAddr string) string {
304	return `{"address":"` + playerAddr + `"}`
305}
306
307func OwnedTools(playerAddr string) string {
308	if !ownedTools.Has(playerAddr) {
309		return `["hands"]`
310	}
311	ids := strings.Split(ownedTools.Get(playerAddr).(string), ",")
312	var quoted []string
313	for _, id := range ids {
314		quoted = append(quoted, `"`+id+`"`)
315	}
316	return "[" + strings.Join(quoted, ",") + "]"
317}
318
319func LifetimeMined(playerAddr string) string {
320	var parts []string
321	lifetimeMined.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
322		coinId := strings.TrimPrefix(key, playerAddr+":")
323		parts = append(parts, `"`+coinId+`":`+val.(string))
324		return false
325	})
326	return "{" + strings.Join(parts, ",") + "}"
327}
328
329func AllMiners(limit int, offset int) string {
330	var parts []string
331	count, skipped := 0, 0
332	miners.Iterate("", "\xFF", func(key string, val any) bool {
333		if skipped < offset {
334			skipped++
335			return false
336		}
337		if count >= limit {
338			return true
339		}
340		display := key
341		if len(display) > 12 {
342			display = display[:8] + "..."
343		}
344		parts = append(parts, `{"address":"`+key+`","display":"`+display+`","total":`+val.(string)+`}`)
345		count++
346		return false
347	})
348	return "[" + strings.Join(parts, ",") + "]"
349}
350
351func GetMedals(playerAddr string) string {
352	var claimed []string
353	achievements.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
354		achId := strings.TrimPrefix(key, playerAddr+":")
355		claimed = append(claimed, `"`+achId+`"`)
356		return false
357	})
358	return ufmt.Sprintf(`{"medals":[%s]}`, strings.Join(claimed, ","))
359}
360
361func DailyCheckinStatus(playerAddr string) string {
362	now := time.Now().Unix()
363	last := int64(0)
364	if dailyCheckins.Has(playerAddr) {
365		last, _ = strconv.ParseInt(dailyCheckins.Get(playerAddr).(string), 10, 64)
366	}
367	done := now-last < 86400
368	return ufmt.Sprintf(`{"ok":true,"done":%t,"streak":1,"next":%d}`, done, last+86400)
369}
370
371func ListTasks(_ string) string { return `{"ok":true,"tasks":[]}` }
372
373func SkillRanks(playerAddr string) string {
374	var parts []string
375	skillRanks.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
376		skillId := strings.TrimPrefix(key, playerAddr+":")
377		parts = append(parts, `"`+skillId+`":`+val.(string))
378		return false
379	})
380	return "{" + strings.Join(parts, ",") + "}"
381}
382
383func ToolLife(playerAddr string) string {
384	var parts []string
385	toolLife.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
386		toolId := strings.TrimPrefix(key, playerAddr+":")
387		parts = append(parts, `"`+toolId+`":`+val.(string))
388		return false
389	})
390	return "{" + strings.Join(parts, ",") + "}"
391}
392
393// ── Render ───────────────────────────────────────────────────────────────────
394
395func Render(_ string) string {
396	return ufmt.Sprintf("# GnoMoji Game Realm\n\nAdmin: `%s`\n\nPath: `gno.land/r/g138gdmwj6heduu3st796lw6qqdwylgaxf7ty5gz/game11`\n\nMine emoji coins on Gno.land!\n", admin.String())
397}
398
399// ── Helpers ──────────────────────────────────────────────────────────────────
400
401// deductBalances atomically checks all balances then deducts them.
402// Panics if any balance is insufficient — no partial deductions.
403// costs is a slice of [coinId, amount] pairs.
404func deductBalances(caller string, costs [][2]string) {
405	// First pass: verify all sufficient
406	for _, pair := range costs {
407		coinId, amtStr := pair[0], pair[1]
408		required, err := strconv.ParseUint(amtStr, 10, 64)
409		if err != nil || required == 0 {
410			continue
411		}
412		balKey := caller + ":" + coinId
413		have := uint64(0)
414		if balances.Has(balKey) {
415			have, _ = strconv.ParseUint(balances.Get(balKey).(string), 10, 64)
416		}
417		if have < required {
418			panic("insufficient " + coinId + ": need " + amtStr + " have " + strconv.FormatUint(have, 10))
419		}
420	}
421	// Second pass: deduct all
422	for _, pair := range costs {
423		coinId, amtStr := pair[0], pair[1]
424		required, err := strconv.ParseUint(amtStr, 10, 64)
425		if err != nil || required == 0 {
426			continue
427		}
428		balKey := caller + ":" + coinId
429		current, _ := strconv.ParseUint(balances.Get(balKey).(string), 10, 64)
430		balances.Set(balKey, strconv.FormatUint(current-required, 10))
431	}
432}
433
434func splitJsonArray(s string) []string {
435	var result []string
436	depth, start := 0, 0
437	for i := 0; i < len(s); i++ {
438		switch s[i] {
439		case '[', '{':
440			depth++
441		case ']', '}':
442			depth--
443		case ',':
444			if depth == 0 {
445				if e := strings.TrimSpace(s[start:i]); e != "" {
446					result = append(result, e)
447				}
448				start = i + 1
449			}
450		}
451	}
452	if e := strings.TrimSpace(s[start:]); e != "" {
453		result = append(result, e)
454	}
455	return result
456}