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.

leaderboard.gno

3.62 Kb · 135 lines
  1// Package leaderboard is an on-chain score leaderboard realm.
  2//
  3// Any account can accumulate points via AddPoints and (optionally) attach a
  4// display name with SetName. Render produces a ranked Markdown table sorted by
  5// points descending, with medals for the top three players.
  6package leaderboard
  7
  8import (
  9	"sort"
 10	"strconv"
 11
 12	"chain/runtime/unsafe"
 13
 14	"gno.land/p/nym-moultest123/kit/ui/v0"
 15	"gno.land/p/nt/avl/v0"
 16)
 17
 18// player holds the accumulated state for a single address.
 19type player struct {
 20	addr   address
 21	name   string
 22	points int
 23}
 24
 25// players maps address string -> *player. An avl.Tree gives deterministic
 26// iteration (unlike a Go map) so Render output is stable across nodes.
 27var players avl.Tree
 28
 29// get returns the stored *player for addr, creating one if absent.
 30func get(addr address) *player {
 31	key := addr.String()
 32	if v := players.Get(key); v != nil {
 33		return v.(*player)
 34	}
 35	p := &player{addr: addr}
 36	players.Set(key, p)
 37	return p
 38}
 39
 40// caller returns the address of the account that invoked the current tx.
 41// unsafe.PreviousRealm() is the origin user for a MsgCall entry point.
 42func caller() address {
 43	return unsafe.PreviousRealm().Address()
 44}
 45
 46// AddPoints adds n points to the caller's total. n must be positive.
 47//
 48// Crossing function: MsgCall dispatches only to crossing functions, and the
 49// cur.IsCurrent() check authenticates the live call frame before we trust the
 50// caller identity derived from it.
 51func AddPoints(cur realm, n int) {
 52	if !cur.IsCurrent() {
 53		panic("spoofed realm")
 54	}
 55	if n <= 0 {
 56		panic("points must be positive")
 57	}
 58	p := get(caller())
 59	p.points += n
 60}
 61
 62// SetName attaches a display name (max 32 bytes) to the caller. Passing an
 63// empty string clears the name and falls back to the address in Render.
 64func SetName(cur realm, name string) {
 65	if !cur.IsCurrent() {
 66		panic("spoofed realm")
 67	}
 68	if len(name) > 32 {
 69		panic("name too long (max 32)")
 70	}
 71	get(caller()).name = name
 72}
 73
 74// display returns the player's name, or a shortened address if unnamed.
 75func (p *player) display() string {
 76	if p.name != "" {
 77		return p.name
 78	}
 79	s := p.addr.String()
 80	if len(s) > 12 {
 81		return s[:8] + "…" + s[len(s)-4:]
 82	}
 83	return s
 84}
 85
 86// byRank implements sort.Interface, ranking players by points descending with
 87// ties broken by address so the order is deterministic across nodes. The gno
 88// sort package exposes Sort/Stable over an Interface — there is no sort.Slice.
 89type byRank []*player
 90
 91func (r byRank) Len() int      { return len(r) }
 92func (r byRank) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
 93func (r byRank) Less(i, j int) bool {
 94	if r[i].points != r[j].points {
 95		return r[i].points > r[j].points
 96	}
 97	return r[i].addr.String() < r[j].addr.String()
 98}
 99
100// snapshot collects all players into a slice ranked by points descending.
101func snapshot() []*player {
102	out := make([]*player, 0, players.Size())
103	players.Iterate("", "", func(_ string, v any) bool {
104		out = append(out, v.(*player))
105		return false
106	})
107	sort.Stable(byRank(out))
108	return out
109}
110
111// Render produces the Markdown leaderboard. It is NOT a crossing function
112// (read-only view, no cur realm parameter).
113func Render(path string) string {
114	ranked := snapshot()
115
116	out := "# 🏆 Leaderboard\n\n"
117	if len(ranked) == 0 {
118		out += "_No players yet. Call `AddPoints(n)` to get on the board._\n"
119		return out
120	}
121
122	out += "Total players: **" + strconv.Itoa(len(ranked)) + "**\n\n"
123	out += "| Rank | Player | Points |\n"
124	out += "| ---: | :--- | ---: |\n"
125	for i, p := range ranked {
126		rankCell := ui.Podium(i)
127		if rankCell == "" {
128			rankCell = strconv.Itoa(i + 1)
129		}
130		out += "| " + rankCell +
131			" | " + p.display() +
132			" | " + strconv.Itoa(p.points) + " |\n"
133	}
134	return out
135}