Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

kingofdice.gno

5.71 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/nym-moultest123/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}