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

9.52 Kb · 312 lines
  1package game29
  2
  3import (
  4	"sort"
  5	"strconv"
  6	"strings"
  7	"time"
  8
  9	"gno.land/p/nt/avl/v0"
 10	"gno.land/p/nt/ufmt/v0"
 11)
 12
 13// ── Guilds ───────────────────────────────────────────────────────────────────
 14
 15const maxGuildMembers = 10
 16const maxGuildNameLen = 24
 17
 18func guildJSON(guildId, name, leader string, members int, createdAt int64) string {
 19	return `{"guildId":` + jsonStr(guildId) + `,"name":` + jsonStr(name) + `,"leader":` + jsonStr(leader) +
 20		`,"memberCount":` + strconv.Itoa(members) + `,"createdAt":` + strconv.FormatInt(createdAt, 10) + `}`
 21}
 22
 23func guildMemberCount(gid string) int {
 24	n := 0
 25	guildMembers.Iterate(gid+":", gid+";", func(string, any) bool {
 26		n++
 27		return false
 28	})
 29	return n
 30}
 31
 32type guildMeta struct {
 33	name, leader string
 34	createdAt    int64
 35}
 36
 37// guild meta is stored alongside the JSON so it can be re-rendered safely.
 38var guildMetas = map[string]guildMeta{}
 39
 40func rewriteGuild(gid string) {
 41	m, ok := guildMetas[gid]
 42	if !ok {
 43		return
 44	}
 45	guilds.Set(gid, guildJSON(gid, m.name, m.leader, guildMemberCount(gid), m.createdAt))
 46}
 47
 48func CreateGuild(cur realm, name string) string {
 49	caller := requirePlayer(cur)
 50	if playerGuild.Has(caller) {
 51		panic("already in a guild")
 52	}
 53	name = sanitizeName(name)
 54	if len(name) < 3 {
 55		panic("guild name too short (min 3 chars)")
 56	}
 57	if len(name) > maxGuildNameLen {
 58		panic("guild name too long (max 24 chars)")
 59	}
 60	guildCounter++
 61	gid := "guild-" + zeroPad(guildCounter, 6)
 62	now := time.Now().Unix()
 63	guildMetas[gid] = guildMeta{name: name, leader: caller, createdAt: now}
 64	guildMembers.Set(gid+":"+caller, "1")
 65	playerGuild.Set(caller, gid)
 66	rewriteGuild(gid)
 67	return gid
 68}
 69
 70func JoinGuild(cur realm, guildId string) {
 71	caller := requirePlayer(cur)
 72	if playerGuild.Has(caller) {
 73		panic("already in a guild — leave current guild first")
 74	}
 75	if !guilds.Has(guildId) {
 76		panic("guild not found")
 77	}
 78	if guildMemberCount(guildId) >= maxGuildMembers {
 79		panic("guild is full (max 10 members)")
 80	}
 81	guildMembers.Set(guildId+":"+caller, "1")
 82	playerGuild.Set(caller, guildId)
 83	rewriteGuild(guildId)
 84}
 85
 86func LeaveGuild(cur realm) {
 87	requireNotPaused()
 88	caller := cur.Previous().Address().String()
 89	if !playerGuild.Has(caller) {
 90		panic("not in a guild")
 91	}
 92	gid := playerGuild.Get(caller).(string)
 93	guildMembers.Remove(gid + ":" + caller)
 94	playerGuild.Remove(caller)
 95	if guildMemberCount(gid) == 0 {
 96		guilds.Remove(gid)
 97		delete(guildMetas, gid)
 98		return
 99	}
100	if m, ok := guildMetas[gid]; ok && m.leader == caller {
101		// hand leadership to the first remaining member
102		guildMembers.Iterate(gid+":", gid+";", func(k string, _ any) bool {
103			m.leader = strings.TrimPrefix(k, gid+":")
104			return true
105		})
106		guildMetas[gid] = m
107	}
108	rewriteGuild(gid)
109}
110
111func PlayerGuild(addr string) string {
112	if !playerGuild.Has(addr) {
113		return ""
114	}
115	gid := playerGuild.Get(addr).(string)
116	if !guilds.Has(gid) {
117		return ""
118	}
119	return guilds.Get(gid).(string)
120}
121
122func GuildInfo(guildId string) string {
123	if !guilds.Has(guildId) {
124		return ""
125	}
126	return guilds.Get(guildId).(string)
127}
128
129func TopGuilds(limit int) string {
130	if limit <= 0 || limit > 50 {
131		limit = 20
132	}
133	var all guildRows
134	guilds.Iterate("", "", func(k string, v any) bool {
135		all = append(all, guildRow{v.(string), guildMemberCount(k)})
136		return false
137	})
138	sort.Stable(all)
139	var parts []string
140	for i := 0; i < len(all) && i < limit; i++ {
141		parts = append(parts, all[i].js)
142	}
143	return "[" + strings.Join(parts, ",") + "]"
144}
145
146// ── Queries ──────────────────────────────────────────────────────────────────
147
148func kvJSON(prefixAddr string, t *avl.Tree) string {
149	var parts []string
150	t.Iterate(prefixAddr+":", prefixAddr+";", func(key string, val any) bool {
151		parts = append(parts, jsonStr(strings.TrimPrefix(key, prefixAddr+":"))+":"+val.(string))
152		return false
153	})
154	return "{" + strings.Join(parts, ",") + "}"
155}
156
157func AllBalances(playerAddr string) string   { return kvJSON(playerAddr, &balances) }
158func LifetimeMined(playerAddr string) string { return kvJSON(playerAddr, &lifetimeMined) }
159
160// PlayerXp returns the caller's cumulative rarity-weighted XP earned from
161// ClaimAndMint calls as a plain decimal string (e.g. "12345"). Returns "0"
162// for players who have not yet minted anything on this realm version.
163func PlayerXp(playerAddr string) string {
164	return strconv.FormatUint(getU(&playerXp, playerAddr), 10)
165}
166
167// Rewarded returns what rewards (medals etc.) credited, per coin. Separate
168// from LifetimeMined.
169func Rewarded(playerAddr string) string { return kvJSON(playerAddr, &rewarded) }
170func SkillRanks(playerAddr string) string    { return kvJSON(playerAddr, &skillRanks) }
171func ToolLife(playerAddr string) string      { return kvJSON(playerAddr, &toolLife) }
172
173func OwnedTools(playerAddr string) string {
174	if !ownedTools.Has(playerAddr) || ownedTools.Get(playerAddr).(string) == "" {
175		return `["hands"]`
176	}
177	var quoted []string
178	for _, id := range strings.Split(ownedTools.Get(playerAddr).(string), ",") {
179		quoted = append(quoted, jsonStr(id))
180	}
181	return "[" + strings.Join(quoted, ",") + "]"
182}
183
184func PlayerProfile(playerAddr string) string {
185	return `{"address":` + jsonStr(playerAddr) + `,"bestTool":` + jsonStr(bestOwnedTool(playerAddr)) +
186		`,"total":` + strconv.FormatUint(getU(&miners, playerAddr), 10) + `,"passExpiry":` + strconv.FormatInt(GnomePassExpiry(playerAddr), 10) + `}`
187}
188
189// AllMiners returns the leaderboard sorted by total mined (desc), paginated.
190// (game28 returned it in address order.)
191func AllMiners(limit int, offset int) string {
192	if limit <= 0 || limit > 200 {
193		limit = 100
194	}
195	if offset < 0 {
196		offset = 0
197	}
198	var rows minerRows
199	miners.Iterate("", "", func(k string, v any) bool {
200		t, _ := strconv.ParseUint(v.(string), 10, 64)
201		rows = append(rows, minerRow{k, t})
202		return false
203	})
204	sort.Sort(rows)
205	var parts []string
206	for i := offset; i < len(rows) && i < offset+limit; i++ {
207		d := rows[i].addr
208		if len(d) > 12 {
209			d = d[:8] + "..."
210		}
211		parts = append(parts, `{"address":`+jsonStr(rows[i].addr)+`,"display":`+jsonStr(d)+`,"total":`+strconv.FormatUint(rows[i].total, 10)+`,"rank":`+strconv.Itoa(i+1)+`}`)
212	}
213	return "[" + strings.Join(parts, ",") + "]"
214}
215
216func GetMedals(playerAddr string) string {
217	var claimed []string
218	achievements.Iterate(playerAddr+":", playerAddr+";", func(key string, _ any) bool {
219		claimed = append(claimed, jsonStr(strings.TrimPrefix(key, playerAddr+":")))
220		return false
221	})
222	return `{"medals":[` + strings.Join(claimed, ",") + `]}`
223}
224
225func DailyCheckinStatus(playerAddr string) string {
226	today := time.Now().Unix() / 86400
227	var lastDay, streak int64
228	if dailyCheckins.Has(playerAddr) {
229		raw := dailyCheckins.Get(playerAddr).(string)
230		lastDay, streak = parseDailyCheckin(raw)
231	}
232	done := lastDay == today
233	// Streak is live only if the player checked in today or yesterday.
234	// If they missed a day, report 0 so the client knows the streak is broken.
235	liveStreak := streak
236	if lastDay < today-1 {
237		liveStreak = 0
238	}
239	nextMidnight := (today + 1) * 86400
240	return ufmt.Sprintf(`{"ok":true,"done":%t,"streak":%d,"next":%d}`, done, liveStreak, nextMidnight)
241}
242
243func ListTasks(_ string) string {
244	var parts []string
245	achievements.Iterate("task:", "task;", func(k string, v any) bool {
246		if strings.HasSuffix(k, ":desc") {
247			id := strings.TrimSuffix(strings.TrimPrefix(k, "task:"), ":desc")
248			parts = append(parts, `{"id":`+jsonStr(id)+`,"desc":`+jsonStr(v.(string))+`}`)
249		}
250		return false
251	})
252	return `{"ok":true,"tasks":[` + strings.Join(parts, ",") + `]}`
253}
254
255// ── Render ───────────────────────────────────────────────────────────────────
256
257func Render(_ string) string {
258	return ufmt.Sprintf("# GnoMoji Game Realm — game29\n\nAdmin: `%s`\n\nPath: `%s`\n\nClaims: server-vouchered (%s)\n\nMigration finalized: %t\n\nMine emoji coins on Gno.land!\n",
259		admin.String(), realmPkgPath, func() string {
260			if len(claimSignerPubKey) == 32 {
261				return "signer set"
262			}
263			return "disabled — no signer"
264		}(), migrationFinalized)
265}
266
267// ── Helpers ──────────────────────────────────────────────────────────────────
268
269// deductBalances checks all balances, then deducts (no partial deductions).
270func deductBalances(caller string, costs [][2]string) {
271	for _, pair := range costs {
272		required, err := strconv.ParseUint(pair[1], 10, 64)
273		if err != nil || required == 0 {
274			continue
275		}
276		if have := balOf(caller, pair[0]); have < required {
277			panic("insufficient " + pair[0] + ": need " + pair[1] + " have " + strconv.FormatUint(have, 10))
278		}
279	}
280	for _, pair := range costs {
281		required, err := strconv.ParseUint(pair[1], 10, 64)
282		if err != nil || required == 0 {
283			continue
284		}
285		debitBal(caller, pair[0], required)
286	}
287}
288
289type guildRow struct {
290	js string
291	n  int
292}
293type guildRows []guildRow
294
295func (g guildRows) Len() int           { return len(g) }
296func (g guildRows) Less(i, j int) bool { return g[i].n > g[j].n }
297func (g guildRows) Swap(i, j int)      { g[i], g[j] = g[j], g[i] }
298
299type minerRow struct {
300	addr  string
301	total uint64
302}
303type minerRows []minerRow
304
305func (m minerRows) Len() int { return len(m) }
306func (m minerRows) Less(i, j int) bool {
307	if m[i].total != m[j].total {
308		return m[i].total > m[j].total
309	}
310	return m[i].addr < m[j].addr
311}
312func (m minerRows) Swap(i, j int) { m[i], m[j] = m[j], m[i] }