Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

leaderboard.gno

3.60 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/moul/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}