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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

closestguess.gno

7.56 Kb · 250 lines
  1// Package closestguess is a blind, multiplayer number-guessing puzzle.
  2//
  3// Unlike a classic higher/lower guessing game, closestguess gives NO feedback
  4// per guess. Each round hides a target in 1..1000 (derived deterministically
  5// from the block height the round started at); every address may submit
  6// exactly one blind Guess for the round. Whenever someone calls Reveal, the
  7// round closes: the target is disclosed, the guess with the smallest absolute
  8// distance wins (ties go to whoever guessed first), the winner is recorded on
  9// the leaderboard, and a fresh round starts immediately.
 10package closestguess
 11
 12import (
 13	"strconv"
 14	"strings"
 15
 16	"chain"
 17	"chain/runtime"
 18
 19	"gno.land/p/nym-moultest123/kit/ui/v0"
 20)
 21
 22const (
 23	minTarget = 1
 24	maxTarget = 1000
 25)
 26
 27// roundT holds the mutable state of the open round. The target stays hidden
 28// (never rendered, never returned) until Reveal closes the round.
 29type roundT struct {
 30	number      int
 31	startHeight int64
 32	target      int
 33}
 34
 35// winRecord is one closed-round result, kept for the leaderboard.
 36type winRecord struct {
 37	round    int
 38	winner   string
 39	target   int
 40	guess    int
 41	distance int
 42	players  int
 43}
 44
 45// guessEntry is one address's blind guess for the current round.
 46type guessEntry struct {
 47	addr string
 48	n    int
 49}
 50
 51var (
 52	current *roundT
 53	// entries holds one guessEntry per address for the CURRENT round only, in
 54	// submission order (so Reveal can break distance ties in favor of
 55	// whoever guessed first). Reset on every Reveal. A plain slice is fine
 56	// here: rounds are player-scoped and small, unlike long-lived global
 57	// state where avl.Tree would matter.
 58	entries []guessEntry
 59	// leaderboard is append-only across all rounds; sorted on render.
 60	leaderboard []winRecord
 61)
 62
 63func init() {
 64	current = newRound(1, runtime.ChainHeight())
 65}
 66
 67// newRound builds a fresh round whose hidden target derives deterministically
 68// from the start height and round number. Gno has no runtime RNG, so mixing
 69// height with the round number is the only available entropy, and it also
 70// keeps consecutive rounds at the same height from repeating a target.
 71func newRound(number int, height int64) *roundT {
 72	return &roundT{
 73		number:      number,
 74		startHeight: height,
 75		target:      targetFromHeight(height, number),
 76	}
 77}
 78
 79func targetFromHeight(h int64, round int) int {
 80	if h < 0 {
 81		h = -h
 82	}
 83	span := int64(maxTarget - minTarget + 1)
 84	mixed := h*2654435761 + int64(round)*40503 + 12345
 85	if mixed < 0 {
 86		mixed = -mixed
 87	}
 88	return int(mixed%span) + minTarget
 89}
 90
 91// Guess records the caller's single blind guess for the current round. It
 92// returns no hint about correctness — that is the whole point of the puzzle.
 93// Crossing function: caller invokes as Guess(cross(cur), n).
 94func Guess(cur realm, n int) string {
 95	if n < minTarget || n > maxTarget {
 96		panic("guess must be in " + strconv.Itoa(minTarget) + ".." + strconv.Itoa(maxTarget))
 97	}
 98
 99	caller := cur.Previous().Address().String()
100	if hasGuessed(caller) {
101		panic("you already guessed this round; wait for Reveal()")
102	}
103
104	entries = append(entries, guessEntry{addr: caller, n: n})
105
106	chain.Emit("GuessSubmitted",
107		"round", strconv.Itoa(current.number),
108		"player", caller,
109	)
110
111	return "guess recorded for round " + strconv.Itoa(current.number) + " (" +
112		strconv.Itoa(len(entries)) + " player(s) so far) — call Reveal() to see who's closest"
113}
114
115func hasGuessed(addr string) bool {
116	for _, e := range entries {
117		if e.addr == addr {
118			return true
119		}
120	}
121	return false
122}
123
124// Reveal closes the current round: discloses the target, crowns whoever
125// landed closest (ties favor the earliest guesser), records a leaderboard
126// entry, and opens a fresh round. Panics if nobody has guessed yet.
127// Crossing function.
128func Reveal(cur realm) string {
129	if len(entries) == 0 {
130		panic("no guesses submitted yet this round")
131	}
132
133	target := current.target
134	winner := entries[0]
135	best := distance(winner.n, target)
136	for _, e := range entries[1:] {
137		if d := distance(e.n, target); d < best {
138			best = d
139			winner = e
140		}
141	}
142
143	rec := winRecord{
144		round:    current.number,
145		winner:   winner.addr,
146		target:   target,
147		guess:    winner.n,
148		distance: best,
149		players:  len(entries),
150	}
151	leaderboard = append(leaderboard, rec)
152
153	chain.Emit("RoundRevealed",
154		"round", strconv.Itoa(rec.round),
155		"winner", winner.addr,
156		"target", strconv.Itoa(target),
157		"distance", strconv.Itoa(best),
158	)
159
160	closedRound := current.number
161	current = newRound(closedRound+1, runtime.ChainHeight())
162	entries = nil
163
164	return "round " + strconv.Itoa(closedRound) + " revealed: target was " + strconv.Itoa(target) +
165		" — " + ui.AddrOf(winner.addr) + " wins, guessed " + strconv.Itoa(winner.n) +
166		" (distance " + strconv.Itoa(best) + ") among " + strconv.Itoa(rec.players) +
167		" player(s). Round " + strconv.Itoa(current.number) + " is now open."
168}
169
170func distance(guess, target int) int {
171	d := guess - target
172	if d < 0 {
173		d = -d
174	}
175	return d
176}
177
178// Render produces the gnoweb Markdown view. The current round's target and
179// individual guess values are never shown — only the player count — so
180// rendering the page can't leak the puzzle.
181func Render(path string) string {
182	var b strings.Builder
183
184	b.WriteString("# Closest Guess\n\n")
185	b.WriteString("A blind, multiplayer number-guessing puzzle. Each round hides a target in ")
186	b.WriteString("**" + strconv.Itoa(minTarget) + ".." + strconv.Itoa(maxTarget) + "**. ")
187	b.WriteString("Call `Guess(n)` once per round — you get no feedback. ")
188	b.WriteString("Anyone can call `Reveal()` to close the round: the target is disclosed and ")
189	b.WriteString("whoever landed closest wins.\n\n")
190
191	b.WriteString("## Current round\n\n")
192	b.WriteString("- Round: **" + strconv.Itoa(current.number) + "**\n")
193	b.WriteString("- Started at block height: " + strconv.FormatInt(current.startHeight, 10) + "\n")
194	b.WriteString("- Players guessed so far: " + strconv.Itoa(len(entries)) + "\n")
195	b.WriteString("- Status: **open** — target hidden until `Reveal()` is called.\n\n")
196
197	b.WriteString("## Leaderboard (closest-ever wins)\n\n")
198	b.WriteString(renderLeaderboard())
199
200	b.WriteString("\n## How to play\n\n")
201	b.WriteString("```\n")
202	b.WriteString("Guess(n)   // n in 1..1000, one shot per address per round, no feedback\n")
203	b.WriteString("Reveal()   // closes the round: reveals target, crowns the closest guess\n")
204	b.WriteString("```\n")
205
206	return b.String()
207}
208
209// renderLeaderboard formats closed rounds ranked by smallest distance (ties:
210// earlier round first). Sorted on a copy so state is untouched.
211func renderLeaderboard() string {
212	if len(leaderboard) == 0 {
213		return "_No round has been revealed yet — be the first to call `Reveal()`!_\n"
214	}
215
216	ranked := make([]winRecord, len(leaderboard))
217	copy(ranked, leaderboard)
218	sortByClosest(ranked)
219
220	var b strings.Builder
221	b.WriteString("| Rank | Player | Round | Target | Guess | Distance | Players |\n")
222	b.WriteString("|---|---|---|---|---|---|---|\n")
223	for i, w := range ranked {
224		b.WriteString("| " + strconv.Itoa(i+1) + " | " + ui.AddrOf(w.winner) + " | " +
225			strconv.Itoa(w.round) + " | " + strconv.Itoa(w.target) + " | " +
226			strconv.Itoa(w.guess) + " | " + strconv.Itoa(w.distance) + " | " +
227			strconv.Itoa(w.players) + " |\n")
228	}
229	return b.String()
230}
231
232// sortByClosest does an in-place insertion sort: smallest distance first,
233// then earlier round first on ties. Small n; insertion sort keeps it
234// deterministic and dependency-free.
235func sortByClosest(rs []winRecord) {
236	for i := 1; i < len(rs); i++ {
237		j := i
238		for j > 0 && closer(rs[j], rs[j-1]) {
239			rs[j], rs[j-1] = rs[j-1], rs[j]
240			j--
241		}
242	}
243}
244
245func closer(a, b winRecord) bool {
246	if a.distance != b.distance {
247		return a.distance < b.distance
248	}
249	return a.round < b.round
250}