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.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 := players.Get(key); v != nil {
 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}