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}