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

closestguess.gno

7.62 Kb · 255 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
 20const (
 21	minTarget = 1
 22	maxTarget = 1000
 23)
 24
 25// roundT holds the mutable state of the open round. The target stays hidden
 26// (never rendered, never returned) until Reveal closes the round.
 27type roundT struct {
 28	number      int
 29	startHeight int64
 30	target      int
 31}
 32
 33// winRecord is one closed-round result, kept for the leaderboard.
 34type winRecord struct {
 35	round    int
 36	winner   string
 37	target   int
 38	guess    int
 39	distance int
 40	players  int
 41}
 42
 43// guessEntry is one address's blind guess for the current round.
 44type guessEntry struct {
 45	addr string
 46	n    int
 47}
 48
 49var (
 50	current *roundT
 51	// entries holds one guessEntry per address for the CURRENT round only, in
 52	// submission order (so Reveal can break distance ties in favor of
 53	// whoever guessed first). Reset on every Reveal. A plain slice is fine
 54	// here: rounds are player-scoped and small, unlike long-lived global
 55	// state where avl.Tree would matter.
 56	entries []guessEntry
 57	// leaderboard is append-only across all rounds; sorted on render.
 58	leaderboard []winRecord
 59)
 60
 61func init() {
 62	current = newRound(1, runtime.ChainHeight())
 63}
 64
 65// newRound builds a fresh round whose hidden target derives deterministically
 66// from the start height and round number. Gno has no runtime RNG, so mixing
 67// height with the round number is the only available entropy, and it also
 68// 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}
 76
 77func targetFromHeight(h int64, round int) int {
 78	if h < 0 {
 79		h = -h
 80	}
 81	span := int64(maxTarget - minTarget + 1)
 82	mixed := h*2654435761 + int64(round)*40503 + 12345
 83	if mixed < 0 {
 84		mixed = -mixed
 85	}
 86	return int(mixed%span) + minTarget
 87}
 88
 89// Guess records the caller's single blind guess for the current round. It
 90// 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	}
 96
 97	caller := cur.Previous().Address().String()
 98	if hasGuessed(caller) {
 99		panic("you already guessed this round; wait for Reveal()")
100	}
101
102	entries = append(entries, guessEntry{addr: caller, n: n})
103
104	chain.Emit("GuessSubmitted",
105		"round", strconv.Itoa(current.number),
106		"player", caller,
107	)
108
109	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}
112
113func hasGuessed(addr string) bool {
114	for _, e := range entries {
115		if e.addr == addr {
116			return true
117		}
118	}
119	return false
120}
121
122// Reveal closes the current round: discloses the target, crowns whoever
123// landed closest (ties favor the earliest guesser), records a leaderboard
124// 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	}
130
131	target := current.target
132	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 = d
137			winner = e
138		}
139	}
140
141	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)
150
151	chain.Emit("RoundRevealed",
152		"round", strconv.Itoa(rec.round),
153		"winner", winner.addr,
154		"target", strconv.Itoa(target),
155		"distance", strconv.Itoa(best),
156	)
157
158	closedRound := current.number
159	current = newRound(closedRound+1, runtime.ChainHeight())
160	entries = nil
161
162	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}
167
168func distance(guess, target int) int {
169	d := guess - target
170	if d < 0 {
171		d = -d
172	}
173	return d
174}
175
176// Render produces the gnoweb Markdown view. The current round's target and
177// individual guess values are never shown — only the player count — so
178// rendering the page can't leak the puzzle.
179func Render(path string) string {
180	var b strings.Builder
181
182	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")
188
189	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")
194
195	b.WriteString("## Leaderboard (closest-ever wins)\n\n")
196	b.WriteString(renderLeaderboard())
197
198	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")
203
204	return b.String()
205}
206
207// 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	}
213
214	ranked := make([]winRecord, len(leaderboard))
215	copy(ranked, leaderboard)
216	sortByClosest(ranked)
217
218	var b strings.Builder
219	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}
229
230// sortByClosest does an in-place insertion sort: smallest distance first,
231// then earlier round first on ties. Small n; insertion sort keeps it
232// deterministic and dependency-free.
233func sortByClosest(rs []winRecord) {
234	for i := 1; i < len(rs); i++ {
235		j := i
236		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}
242
243func closer(a, b winRecord) bool {
244	if a.distance != b.distance {
245		return a.distance < b.distance
246	}
247	return a.round < b.round
248}
249
250func shortAddr(a string) string {
251	if len(a) <= 12 {
252		return a
253	}
254	return a[:6] + "…" + a[len(a)-4:]
255}