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