kingofdice.gno
5.89 Kb · 214 lines
1// Package kingofdice is a "king of the hill" dice game. Anyone can Roll a
2// pseudo-random 1-100 die; beat the reigning king's roll and you dethrone
3// them, becoming king yourself. The leaderboard doesn't rank raw points — it
4// ranks accumulated *reign length*, measured in blocks held as king, so
5// camping on a lucky high roll pays off more than a single spike.
6package kingofdice
7
8import (
9 "sort"
10 "strconv"
11
12 "chain/runtime"
13
14 "gno.land/p/nt/avl/v0"
15)
16
17// playerStats is the persisted record for one address.
18type playerStats struct {
19 addr address
20 rolls int
21 bestRoll int
22 reigns int
23 totalReignBlocks int64 // blocks held as king across all past reigns
24}
25
26var (
27 players avl.Tree // address string -> *playerStats
28
29 king address
30 kingRoll int
31 reignSince int64 // chain height at which the current king took the throne
32
33 nonce int
34 totalRolls int
35)
36
37// get returns the stored *playerStats for addr, creating one if absent.
38func get(addr address) *playerStats {
39 key := addr.String()
40 if v := players.Get(key); v != nil {
41 return v.(*playerStats)
42 }
43 ps := &playerStats{addr: addr}
44 players.Set(key, ps)
45 return ps
46}
47
48// pseudoRoll derives a deterministic 1-100 value from the caller's address,
49// a per-call nonce, and the current chain height, so no two calls in the
50// same block from the same address collide, yet the result can't be
51// pre-computed off-chain before the triggering transaction lands.
52func pseudoRoll(addr address, nonce int, height int64) int {
53 s := addr.String()
54 var h int64
55 for i := 0; i < len(s); i++ {
56 h = h*31 + int64(s[i])
57 }
58 h += int64(nonce)*104729 + height*7919
59 if h < 0 {
60 h = -h
61 }
62 return int(h%100) + 1
63}
64
65// Roll throws the die for the caller. Rolling higher than the current king's
66// roll dethrones them (crediting their finished reign in blocks) and crowns
67// the caller as the new king.
68func Roll(cur realm) string {
69 if !cur.IsCurrent() {
70 panic("spoofed realm")
71 }
72 caller := cur.Previous().Address()
73
74 nonce++
75 totalRolls++
76 height := runtime.ChainHeight()
77 roll := pseudoRoll(caller, nonce, height)
78
79 ps := get(caller)
80 ps.rolls++
81 if roll > ps.bestRoll {
82 ps.bestRoll = roll
83 }
84
85 if !king.IsValid() || roll > kingRoll {
86 if king.IsValid() {
87 prev := get(king)
88 prev.totalReignBlocks += height - reignSince
89 }
90 king = caller
91 kingRoll = roll
92 reignSince = height
93 ps.reigns++
94 return "you rolled " + strconv.Itoa(roll) + " and seized the throne!"
95 }
96
97 if king == caller {
98 return "you rolled " + strconv.Itoa(roll) + " — you're still king (need to beat " +
99 strconv.Itoa(kingRoll) + ")"
100 }
101 return "you rolled " + strconv.Itoa(roll) + " — not enough to dethrone the king's " +
102 strconv.Itoa(kingRoll)
103}
104
105// effectiveReignBlocks returns a player's accumulated reign length, including
106// the currently-in-progress reign if they are the sitting king. It does not
107// mutate state — it's a display-time projection.
108func effectiveReignBlocks(ps *playerStats, height int64) int64 {
109 if king.IsValid() && ps.addr == king {
110 return ps.totalReignBlocks + (height - reignSince)
111 }
112 return ps.totalReignBlocks
113}
114
115// byReign implements sort.Interface, ranking players by effective reign
116// length descending, ties broken by best roll then address so the order is
117// fully deterministic.
118type byReign struct {
119 stats []*playerStats
120 blocks []int64
121}
122
123func (r byReign) Len() int { return len(r.stats) }
124func (r byReign) Swap(i, j int) {
125 r.stats[i], r.stats[j] = r.stats[j], r.stats[i]
126 r.blocks[i], r.blocks[j] = r.blocks[j], r.blocks[i]
127}
128func (r byReign) Less(i, j int) bool {
129 if r.blocks[i] != r.blocks[j] {
130 return r.blocks[i] > r.blocks[j]
131 }
132 if r.stats[i].bestRoll != r.stats[j].bestRoll {
133 return r.stats[i].bestRoll > r.stats[j].bestRoll
134 }
135 return r.stats[i].addr.String() < r.stats[j].addr.String()
136}
137
138// snapshot collects all players ranked by effective reign length descending.
139func snapshot(height int64) ([]*playerStats, []int64) {
140 stats := make([]*playerStats, 0, players.Size())
141 players.Iterate("", "", func(_ string, v any) bool {
142 stats = append(stats, v.(*playerStats))
143 return false
144 })
145 blocks := make([]int64, len(stats))
146 for i, ps := range stats {
147 blocks[i] = effectiveReignBlocks(ps, height)
148 }
149 sort.Stable(byReign{stats: stats, blocks: blocks})
150 return stats, blocks
151}
152
153// medal returns the emoji for a given zero-based rank, or "" past the podium.
154func medal(rank int) string {
155 switch rank {
156 case 0:
157 return "🥇"
158 case 1:
159 return "🥈"
160 case 2:
161 return "🥉"
162 default:
163 return ""
164 }
165}
166
167// display returns a shortened address for table display.
168func display(addr address) string {
169 s := addr.String()
170 if len(s) > 12 {
171 return s[:8] + "…" + s[len(s)-4:]
172 }
173 return s
174}
175
176// Render shows the current king and a reign-length leaderboard.
177func Render(path string) string {
178 height := runtime.ChainHeight()
179
180 out := "# 👑 King of the Dice\n\n"
181 out += "Roll higher than the reigning king to take the throne. The leaderboard " +
182 "ranks total blocks held as king, not raw roll count — outlasting beats " +
183 "getting lucky once.\n\n"
184
185 if !king.IsValid() {
186 out += "_No king yet. Call `Roll()` to make the first claim._\n\n"
187 } else {
188 out += "**Current king:** `" + display(king) + "` with a roll of **" +
189 strconv.Itoa(kingRoll) + "**, reigning for **" +
190 strconv.Itoa(int(height-reignSince)) + "** blocks so far.\n\n"
191 }
192 out += "Total rolls: **" + strconv.Itoa(totalRolls) + "**\n\n"
193
194 stats, blocks := snapshot(height)
195 if len(stats) == 0 {
196 return out
197 }
198
199 out += "| Rank | Player | Blocks reigned | Reigns | Best roll | Rolls |\n"
200 out += "| ---: | :--- | ---: | ---: | ---: | ---: |\n"
201 for i, ps := range stats {
202 rankCell := medal(i)
203 if rankCell == "" {
204 rankCell = strconv.Itoa(i + 1)
205 }
206 out += "| " + rankCell +
207 " | " + display(ps.addr) +
208 " | " + strconv.Itoa(int(blocks[i])) +
209 " | " + strconv.Itoa(ps.reigns) +
210 " | " + strconv.Itoa(ps.bestRoll) +
211 " | " + strconv.Itoa(ps.rolls) + " |\n"
212 }
213 return out
214}