Search Apps Documentation Source Content File Folder Download Copy Actions Download

leaderboard.gno

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