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.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}