Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}