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 result4// deterministically. Instead, each roll derives its 1-6 face from public block5// entropy (chain height) mixed with the caller address and the caller's roll6// count. The derivation is fully reproducible from public data, which is what7// "provably fair" means here — anyone can recompute a roll and verify it.8package dice910import (11 "chain"12 "chain/runtime"13 "strconv"14 "strings"1516 "gno.land/p/nt/avl/v0"17)1819// history holds every roll made by a single caller, most recent last.20type history struct {21 rolls []int // each value in [1,6]22}2324var (25 // count[i] is the total number of times face (i+1) has come up globally.26 count [6]int27 // total is the total number of rolls across all callers.28 total int29 // players maps caller address string -> *history (deterministic iteration).30 players = avl.NewTree()31)3233// 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 can35// recompute and verify a roll. Uses a simple FNV-1a-style hash over the mixed36// 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 := offset43 mix := func(v uint64) {44 for i := 0; i < 8; i++ {45 h ^= v & 0xff46 h *= prime47 v >>= 848 }49 }50 mix(uint64(height))51 mix(uint64(n))52 for i := 0; i < len(addr); i++ {53 h ^= uint64(addr[i])54 h *= prime55 }56 return int(h%6) + 157}5859// 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` first62// parameter). Callers invoke it as Roll(cross(cur)). The result is derived63// deterministically from the current block height, the caller address, and the64// 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()7071 var h *history72 if v := players.Get(addr); v != nil {73 h = v.(*history)74 } else {75 h = &history{}76 players.Set(addr, h)77 }7879 face := deriveFace(runtime.ChainHeight(), addr, len(h.rolls))80 h.rolls = append(h.rolls, face)81 count[face-1]++82 total++8384 chain.Emit("Roll", "player", addr, "face", strconv.Itoa(face))85 return face86}8788// 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 nil94 }95 out := v.(*history).rolls96 cp := make([]int, len(out))97 copy(cp, out)98 return cp99}100101// Total returns the global roll count.102func Total() int { return total }103104// Distribution returns the global per-face counts, faces 1-6.105func Distribution() [6]int { return count }106107// 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 / max113 if filled > width {114 filled = width115 }116 return strings.Repeat("▓", filled) + strings.Repeat("░", width-filled)117}118119// Render returns the realm's gnoweb Markdown view.120//121// Render is NOT a crossing function (no `cur realm` param) — it is a read-only122// query surface. It shows the total roll count, a distribution bar chart for123// faces 1-6, and the most recent rolls.124func Render(path string) string {125 var b strings.Builder126 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")130131 b.WriteString("**Total rolls:** " + strconv.Itoa(total) + "\n\n")132133 // Distribution bar chart.134 b.WriteString("## Distribution\n\n")135 max := 0136 for _, c := range count {137 if c > max {138 max = c139 }140 }141 for face := 1; face <= 6; face++ {142 c := count[face-1]143 pct := 0144 if total > 0 {145 pct = c * 100 / total146 }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 }152153 // 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 string161 face int162 }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 player167 start := len(h.rolls) - 3168 if start < 0 {169 start = 0170 }171 for _, f := range h.rolls[start:] {172 recent = append(recent, entry{addr: key, face: f})173 }174 return false175 })176 // show at most the last 10 collected177 start := len(recent) - 10178 if start < 0 {179 start = 0180 }181 for _, e := range recent[start:] {182 short := e.addr183 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}190Distribution() [6]int
Render(path string) string
Roll(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int
RollsOf(addr string) []int
Total() int
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.