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

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}