← Back to Realms

Realm

gno.land/r/moul/x/daily/dice/v0

Overview

Realm Path
gno.land/r/moul/x/daily/dice/v0
Exported Functions
5
State Entries
4
Source Files
2
Total Package Entries
11

Exported Functions

5 exported functions

State

4 state entries

Source Code

FILES
dice.gno
go
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}
190

Raw Package Data

Raw JSON data