1// Package closestguess is a blind, multiplayer number-guessing puzzle.2//3// Unlike a classic higher/lower guessing game, closestguess gives NO feedback4// per guess. Each round hides a target in 1..1000 (derived deterministically5// from the block height the round started at); every address may submit6// exactly one blind Guess for the round. Whenever someone calls Reveal, the7// round closes: the target is disclosed, the guess with the smallest absolute8// distance wins (ties go to whoever guessed first), the winner is recorded on9// the leaderboard, and a fresh round starts immediately.10package closestguess1112import (13 "strconv"14 "strings"1516 "chain"17 "chain/runtime"18)1920const (21 minTarget = 122 maxTarget = 100023)2425// roundT holds the mutable state of the open round. The target stays hidden26// (never rendered, never returned) until Reveal closes the round.27type roundT struct {28 number int29 startHeight int6430 target int31}3233// winRecord is one closed-round result, kept for the leaderboard.34type winRecord struct {35 round int36 winner string37 target int38 guess int39 distance int40 players int41}4243// guessEntry is one address's blind guess for the current round.44type guessEntry struct {45 addr string46 n int47}4849var (50 current *roundT51 // entries holds one guessEntry per address for the CURRENT round only, in52 // submission order (so Reveal can break distance ties in favor of53 // whoever guessed first). Reset on every Reveal. A plain slice is fine54 // here: rounds are player-scoped and small, unlike long-lived global55 // state where avl.Tree would matter.56 entries []guessEntry57 // leaderboard is append-only across all rounds; sorted on render.58 leaderboard []winRecord59)6061func init() {62 current = newRound(1, runtime.ChainHeight())63}6465// newRound builds a fresh round whose hidden target derives deterministically66// from the start height and round number. Gno has no runtime RNG, so mixing67// height with the round number is the only available entropy, and it also68// keeps consecutive rounds at the same height from repeating a target.69func newRound(number int, height int64) *roundT {70 return &roundT{71 number: number,72 startHeight: height,73 target: targetFromHeight(height, number),74 }75}7677func targetFromHeight(h int64, round int) int {78 if h < 0 {79 h = -h80 }81 span := int64(maxTarget - minTarget + 1)82 mixed := h*2654435761 + int64(round)*40503 + 1234583 if mixed < 0 {84 mixed = -mixed85 }86 return int(mixed%span) + minTarget87}8889// Guess records the caller's single blind guess for the current round. It90// returns no hint about correctness — that is the whole point of the puzzle.91// Crossing function: caller invokes as Guess(cross(cur), n).92func Guess(cur realm, n int) string {93 if n < minTarget || n > maxTarget {94 panic("guess must be in " + strconv.Itoa(minTarget) + ".." + strconv.Itoa(maxTarget))95 }9697 caller := cur.Previous().Address().String()98 if hasGuessed(caller) {99 panic("you already guessed this round; wait for Reveal()")100 }101102 entries = append(entries, guessEntry{addr: caller, n: n})103104 chain.Emit("GuessSubmitted",105 "round", strconv.Itoa(current.number),106 "player", caller,107 )108109 return "guess recorded for round " + strconv.Itoa(current.number) + " (" +110 strconv.Itoa(len(entries)) + " player(s) so far) — call Reveal() to see who's closest"111}112113func hasGuessed(addr string) bool {114 for _, e := range entries {115 if e.addr == addr {116 return true117 }118 }119 return false120}121122// Reveal closes the current round: discloses the target, crowns whoever123// landed closest (ties favor the earliest guesser), records a leaderboard124// entry, and opens a fresh round. Panics if nobody has guessed yet.125// Crossing function.126func Reveal(cur realm) string {127 if len(entries) == 0 {128 panic("no guesses submitted yet this round")129 }130131 target := current.target132 winner := entries[0]133 best := distance(winner.n, target)134 for _, e := range entries[1:] {135 if d := distance(e.n, target); d < best {136 best = d137 winner = e138 }139 }140141 rec := winRecord{142 round: current.number,143 winner: winner.addr,144 target: target,145 guess: winner.n,146 distance: best,147 players: len(entries),148 }149 leaderboard = append(leaderboard, rec)150151 chain.Emit("RoundRevealed",152 "round", strconv.Itoa(rec.round),153 "winner", winner.addr,154 "target", strconv.Itoa(target),155 "distance", strconv.Itoa(best),156 )157158 closedRound := current.number159 current = newRound(closedRound+1, runtime.ChainHeight())160 entries = nil161162 return "round " + strconv.Itoa(closedRound) + " revealed: target was " + strconv.Itoa(target) +163 " — " + shortAddr(winner.addr) + " wins, guessed " + strconv.Itoa(winner.n) +164 " (distance " + strconv.Itoa(best) + ") among " + strconv.Itoa(rec.players) +165 " player(s). Round " + strconv.Itoa(current.number) + " is now open."166}167168func distance(guess, target int) int {169 d := guess - target170 if d < 0 {171 d = -d172 }173 return d174}175176// Render produces the gnoweb Markdown view. The current round's target and177// individual guess values are never shown — only the player count — so178// rendering the page can't leak the puzzle.179func Render(path string) string {180 var b strings.Builder181182 b.WriteString("# Closest Guess\n\n")183 b.WriteString("A blind, multiplayer number-guessing puzzle. Each round hides a target in ")184 b.WriteString("**" + strconv.Itoa(minTarget) + ".." + strconv.Itoa(maxTarget) + "**. ")185 b.WriteString("Call `Guess(n)` once per round — you get no feedback. ")186 b.WriteString("Anyone can call `Reveal()` to close the round: the target is disclosed and ")187 b.WriteString("whoever landed closest wins.\n\n")188189 b.WriteString("## Current round\n\n")190 b.WriteString("- Round: **" + strconv.Itoa(current.number) + "**\n")191 b.WriteString("- Started at block height: " + strconv.FormatInt(current.startHeight, 10) + "\n")192 b.WriteString("- Players guessed so far: " + strconv.Itoa(len(entries)) + "\n")193 b.WriteString("- Status: **open** — target hidden until `Reveal()` is called.\n\n")194195 b.WriteString("## Leaderboard (closest-ever wins)\n\n")196 b.WriteString(renderLeaderboard())197198 b.WriteString("\n## How to play\n\n")199 b.WriteString("```\n")200 b.WriteString("Guess(n) // n in 1..1000, one shot per address per round, no feedback\n")201 b.WriteString("Reveal() // closes the round: reveals target, crowns the closest guess\n")202 b.WriteString("```\n")203204 return b.String()205}206207// renderLeaderboard formats closed rounds ranked by smallest distance (ties:208// earlier round first). Sorted on a copy so state is untouched.209func renderLeaderboard() string {210 if len(leaderboard) == 0 {211 return "_No round has been revealed yet — be the first to call `Reveal()`!_\n"212 }213214 ranked := make([]winRecord, len(leaderboard))215 copy(ranked, leaderboard)216 sortByClosest(ranked)217218 var b strings.Builder219 b.WriteString("| Rank | Player | Round | Target | Guess | Distance | Players |\n")220 b.WriteString("|---|---|---|---|---|---|---|\n")221 for i, w := range ranked {222 b.WriteString("| " + strconv.Itoa(i+1) + " | `" + shortAddr(w.winner) + "` | " +223 strconv.Itoa(w.round) + " | " + strconv.Itoa(w.target) + " | " +224 strconv.Itoa(w.guess) + " | " + strconv.Itoa(w.distance) + " | " +225 strconv.Itoa(w.players) + " |\n")226 }227 return b.String()228}229230// sortByClosest does an in-place insertion sort: smallest distance first,231// then earlier round first on ties. Small n; insertion sort keeps it232// deterministic and dependency-free.233func sortByClosest(rs []winRecord) {234 for i := 1; i < len(rs); i++ {235 j := i236 for j > 0 && closer(rs[j], rs[j-1]) {237 rs[j], rs[j-1] = rs[j-1], rs[j]238 j--239 }240 }241}242243func closer(a, b winRecord) bool {244 if a.distance != b.distance {245 return a.distance < b.distance246 }247 return a.round < b.round248}249250func shortAddr(a string) string {251 if len(a) <= 12 {252 return a253 }254 return a[:6] + "…" + a[len(a)-4:]255}256Guess(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}, n int) string
Render(path string) string
Reveal(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}) string
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.