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}