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

dice.gno

5.21 Kb · 189 lines
  1// Package dice implements a provably-fair on-chain dice game for gno.land.
  2//
  3// There is no true randomness on-chain: every node must reach the same result
  4// deterministically. Instead, each roll derives its 1-6 face from public block
  5// entropy (chain height) mixed with the caller address and the caller's roll
  6// count. The derivation is fully reproducible from public data, which is what
  7// "provably fair" means here — anyone can recompute a roll and verify it.
  8package dice
  9
 10import (
 11	"chain"
 12	"chain/runtime"
 13	"strconv"
 14	"strings"
 15
 16	"gno.land/p/nt/avl/v0"
 17)
 18
 19// history holds every roll made by a single caller, most recent last.
 20type history struct {
 21	rolls []int // each value in [1,6]
 22}
 23
 24var (
 25	// count[i] is the total number of times face (i+1) has come up globally.
 26	count [6]int
 27	// total is the total number of rolls across all callers.
 28	total int
 29	// players maps caller address string -> *history (deterministic iteration).
 30	players = avl.NewTree()
 31)
 32
 33// deriveFace turns public entropy into a face in [1,6]. It is deterministic:
 34// the same (height, addr, n) always yields the same face, so any observer can
 35// recompute and verify a roll. Uses a simple FNV-1a-style hash over the mixed
 36// entropy bytes so different callers at the same height get different faces.
 37func deriveFace(height int64, addr string, n int) int {
 38	const (
 39		offset = uint64(1469598103934665603)
 40		prime  = uint64(1099511628211)
 41	)
 42	h := offset
 43	mix := func(v uint64) {
 44		for i := 0; i < 8; i++ {
 45			h ^= v & 0xff
 46			h *= prime
 47			v >>= 8
 48		}
 49	}
 50	mix(uint64(height))
 51	mix(uint64(n))
 52	for i := 0; i < len(addr); i++ {
 53		h ^= uint64(addr[i])
 54		h *= prime
 55	}
 56	return int(h%6) + 1
 57}
 58
 59// Roll performs one dice roll for the calling account and records it.
 60//
 61// It is a crossing function (gno 0.9 interrealm convention: `cur realm` first
 62// parameter). Callers invoke it as Roll(cross(cur)). The result is derived
 63// deterministically from the current block height, the caller address, and the
 64// caller's prior roll count — no stored secret, fully verifiable.
 65func Roll(cur realm) int {
 66	if !cur.IsCurrent() {
 67		panic("dice: spoofed realm value")
 68	}
 69	addr := cur.Previous().Address().String()
 70
 71	var h *history
 72	if v := players.Get(addr); v != nil {
 73		h = v.(*history)
 74	} else {
 75		h = &history{}
 76		players.Set(addr, h)
 77	}
 78
 79	face := deriveFace(runtime.ChainHeight(), addr, len(h.rolls))
 80	h.rolls = append(h.rolls, face)
 81	count[face-1]++
 82	total++
 83
 84	chain.Emit("Roll", "player", addr, "face", strconv.Itoa(face))
 85	return face
 86}
 87
 88// RollsOf returns the recorded roll history for an address (oldest first).
 89// Read-only helper, safe to call as a query.
 90func RollsOf(addr string) []int {
 91	v := players.Get(addr)
 92	if v == nil {
 93		return nil
 94	}
 95	out := v.(*history).rolls
 96	cp := make([]int, len(out))
 97	copy(cp, out)
 98	return cp
 99}
100
101// Total returns the global roll count.
102func Total() int { return total }
103
104// Distribution returns the global per-face counts, faces 1-6.
105func Distribution() [6]int { return count }
106
107// bar renders a proportional ▓/░ bar of fixed width for a face's share.
108func bar(n, max, width int) string {
109	if max <= 0 {
110		return strings.Repeat("░", width)
111	}
112	filled := n * width / max
113	if filled > width {
114		filled = width
115	}
116	return strings.Repeat("▓", filled) + strings.Repeat("░", width-filled)
117}
118
119// Render returns the realm's gnoweb Markdown view.
120//
121// Render is NOT a crossing function (no `cur realm` param) — it is a read-only
122// query surface. It shows the total roll count, a distribution bar chart for
123// faces 1-6, and the most recent rolls.
124func Render(path string) string {
125	var b strings.Builder
126	b.WriteString("# 🎲 Provably-Fair Dice\n\n")
127	b.WriteString("Every roll is derived deterministically from the block height, ")
128	b.WriteString("the caller address, and their roll count — no hidden randomness, ")
129	b.WriteString("fully reproducible from public data.\n\n")
130
131	b.WriteString("**Total rolls:** " + strconv.Itoa(total) + "\n\n")
132
133	// Distribution bar chart.
134	b.WriteString("## Distribution\n\n")
135	max := 0
136	for _, c := range count {
137		if c > max {
138			max = c
139		}
140	}
141	for face := 1; face <= 6; face++ {
142		c := count[face-1]
143		pct := 0
144		if total > 0 {
145			pct = c * 100 / total
146		}
147		b.WriteString("`" + strconv.Itoa(face) + "` ")
148		b.WriteString(bar(c, max, 20))
149		b.WriteString(" " + strconv.Itoa(c))
150		b.WriteString(" (" + strconv.Itoa(pct) + "%)\n\n")
151	}
152
153	// Recent rolls: walk players, collect last rolls, show most recent globally.
154	b.WriteString("## Recent rolls\n\n")
155	if total == 0 {
156		b.WriteString("_No rolls yet. Call `Roll` to play._\n")
157		return b.String()
158	}
159	type entry struct {
160		addr string
161		face int
162	}
163	recent := []entry{}
164	players.Iterate("", "", func(key string, value interface{}) bool {
165		h := value.(*history)
166		// take up to the last 3 rolls of each player
167		start := len(h.rolls) - 3
168		if start < 0 {
169			start = 0
170		}
171		for _, f := range h.rolls[start:] {
172			recent = append(recent, entry{addr: key, face: f})
173		}
174		return false
175	})
176	// show at most the last 10 collected
177	start := len(recent) - 10
178	if start < 0 {
179		start = 0
180	}
181	for _, e := range recent[start:] {
182		short := e.addr
183		if len(short) > 12 {
184			short = short[:8] + "…" + short[len(short)-4:]
185		}
186		b.WriteString("- 🎲 **" + strconv.Itoa(e.face) + "** — `" + short + "`\n")
187	}
188	return b.String()
189}