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.

reads.gno

6.22 Kb · 201 lines
  1package memba_arcade_leaderboard_v1
  2
  3import (
  4	"strings"
  5
  6	"gno.land/p/nt/ufmt/v0"
  7)
  8
  9// ── Read getters (pure, non-failing; served via vm/qeval or Render) ─────────────
 10
 11// GetOwner returns the current owner.
 12func GetOwner() address { return owner }
 13
 14// IsPaused reports whether attestation is frozen.
 15func IsPaused() bool { return paused }
 16
 17// IsAttester reports whether an address is on the attester allowlist.
 18func IsAttester(addr string) bool { return isAttester(addr) }
 19
 20// BoardCount returns how many addresses hold an entry on a game's board for a day (O(1)).
 21func BoardCount(game, day string) int {
 22	if v, ok := boardCount.Get(game + "|" + day); ok {
 23		return v.(int)
 24	}
 25	return 0
 26}
 27
 28// entryJSON emits one entry. `stats` is the attester's opaque JSON blob and is emitted as an
 29// ESCAPED STRING FIELD — never spliced raw into the document — so a malformed blob can corrupt
 30// nothing but itself.
 31func entryJSON(e *Entry) string {
 32	return ufmt.Sprintf(
 33		`{"addr":"%s","game":"%s","day":"%s","mode":"%s","score":%d,"stats":"%s","simVersion":%d,"stateHash":"%s","inputLogSha256":"%s","attestedAt":%d}`,
 34		jsonEscape(e.Addr.String()), jsonEscape(e.Game), jsonEscape(e.Day), jsonEscape(e.Mode),
 35		e.Score, jsonEscape(e.Stats), e.SimVersion, jsonEscape(e.StateHash),
 36		jsonEscape(e.InputLogSha256), e.AttestedAt)
 37}
 38
 39// GetEntryJSON returns one address's board entry for a game-day, or "null".
 40func GetEntryJSON(game, day, addr string) string {
 41	if v, ok := boards.Get(game + "|" + day + "|" + addr); ok {
 42		return entryJSON(v.(*Entry))
 43	}
 44	return "null"
 45}
 46
 47// dayEntries collects a game-day's board entries in canonical rank order: score desc,
 48// then earlier attestation, then address — a total order, so pagination is stable.
 49func dayEntries(game, day string) []*Entry {
 50	var out []*Entry
 51	prefix := game + "|" + day + "|"
 52	boards.Iterate(prefix, prefix+"~", func(_ string, v any) bool {
 53		out = append(out, v.(*Entry))
 54		return false
 55	})
 56	// Insertion sort (gno stdlib has no sort.Slice): a day's board is bounded
 57	// and reads are qeval-side — O(n²) on a few hundred entries is fine.
 58	for i := 1; i < len(out); i++ {
 59		e := out[i]
 60		j := i - 1
 61		for j >= 0 && ranksAfter(out[j], e) {
 62			out[j+1] = out[j]
 63			j--
 64		}
 65		out[j+1] = e
 66	}
 67	return out
 68}
 69
 70// ranksAfter reports whether a should sit AFTER b in rank order — a total
 71// order (score desc, then earlier attestation, then address) so pagination
 72// is stable and identical everywhere.
 73func ranksAfter(a, b *Entry) bool {
 74	if a.Score != b.Score {
 75		return a.Score < b.Score
 76	}
 77	if a.AttestedAt != b.AttestedAt {
 78		return a.AttestedAt > b.AttestedAt
 79	}
 80	return a.Addr.String() > b.Addr.String()
 81}
 82
 83// GetBoardJSON returns a page of a game-day's board, rank-ordered, as
 84// {"game":…,"day":…,"total":N,"entries":[…]}. Reads sort the day's entries on demand —
 85// fine for a testnet-scale board; a rank index (points_v1-style) is the v2
 86// upgrade if daily boards grow past a few thousand entries.
 87func GetBoardJSON(game, day string, offset, limit int) string {
 88	all := dayEntries(game, day)
 89	if offset < 0 {
 90		offset = 0
 91	}
 92	if limit <= 0 || limit > 100 {
 93		limit = 100
 94	}
 95	var sb strings.Builder
 96	sb.WriteString(ufmt.Sprintf(`{"game":"%s","day":"%s","total":%d,"entries":[`,
 97		jsonEscape(game), jsonEscape(day), len(all)))
 98	for i := offset; i < len(all) && i < offset+limit; i++ {
 99		if i > offset {
100			sb.WriteString(",")
101		}
102		sb.WriteString(entryJSON(all[i]))
103	}
104	sb.WriteString("]}")
105	return sb.String()
106}
107
108// GetReceiptsJSON returns a page of one address's training receipts (all games, newest first) as
109// {"addr":…,"total":N,"entries":[…]}.
110func GetReceiptsJSON(addr string, offset, limit int) string {
111	var ring []*Entry
112	if v, ok := receipts.Get(addr); ok {
113		ring = v.([]*Entry)
114	}
115	if offset < 0 {
116		offset = 0
117	}
118	if limit <= 0 || limit > 100 {
119		limit = 100
120	}
121	var sb strings.Builder
122	sb.WriteString(ufmt.Sprintf(`{"addr":"%s","total":%d,"entries":[`, jsonEscape(addr), len(ring)))
123	n := 0
124	for i := len(ring) - 1 - offset; i >= 0 && n < limit; i-- {
125		if n > 0 {
126			sb.WriteString(",")
127		}
128		sb.WriteString(entryJSON(ring[i]))
129		n++
130	}
131	sb.WriteString("]}")
132	return sb.String()
133}
134
135// Render serves a human-readable snapshot: `:board/<game>/YYYY-MM-DD` for a game-day's top
136// entries, `:receipts/<addr>` for an address's latest receipts.
137func Render(path string) string {
138	if strings.HasPrefix(path, "board/") {
139		rest := path[len("board/"):]
140		slash := strings.Index(rest, "/")
141		if slash < 0 {
142			return "# memba_arcade_leaderboard_v1\n\nUse `:board/<game>/YYYY-MM-DD`.\n"
143		}
144		game := rest[:slash]
145		day := rest[slash+1:]
146		all := dayEntries(game, day)
147		var sb strings.Builder
148		sb.WriteString("# " + jsonEscape(game) + " daily board — " + jsonEscape(day) + "\n\n")
149		if len(all) == 0 {
150			sb.WriteString("No attested runs yet.\n")
151			return sb.String()
152		}
153		for i, e := range all {
154			if i >= 50 {
155				sb.WriteString(ufmt.Sprintf("\n…and %d more.\n", len(all)-50))
156				break
157			}
158			line := ufmt.Sprintf("%d. `%s` — **%d**", i+1, e.Addr.String(), e.Score)
159			if e.Stats != "" {
160				line += " · " + jsonEscape(e.Stats)
161			}
162			sb.WriteString(line + "\n")
163		}
164		return sb.String()
165	}
166	if strings.HasPrefix(path, "receipts/") {
167		addr := path[len("receipts/"):]
168		return "# Training receipts — " + addr + "\n\n```json\n" + GetReceiptsJSON(addr, 0, 20) + "\n```\n"
169	}
170	return "# memba_arcade_leaderboard_v1\n\nVerified Memba arcade results — never a client-claimed number.\n\n" +
171		"- `:board/<game>/YYYY-MM-DD` — a game's competitive board for a day\n" +
172		"- `:receipts/<addr>` — an address's training receipts (all games)\n"
173}
174
175// jsonEscape escapes a string for embedding in a JSON string literal (defensive — addresses are
176// bech32 and hashes are hex, but the read path escapes regardless; the Render path reuses it to
177// neutralize newlines/control chars in attester-authored stats).
178func jsonEscape(s string) string {
179	var sb strings.Builder
180	for _, r := range s {
181		switch r {
182		case '"':
183			sb.WriteString("\\\"")
184		case '\\':
185			sb.WriteString("\\\\")
186		case '\n':
187			sb.WriteString("\\n")
188		case '\r':
189			sb.WriteString("\\r")
190		case '\t':
191			sb.WriteString("\\t")
192		default:
193			if r < 0x20 {
194				sb.WriteString(ufmt.Sprintf("\\u%04x", r))
195			} else {
196				sb.WriteString(string(r))
197			}
198		}
199	}
200	return sb.String()
201}