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

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}