← Back to Realms

Realm

gno.land/r/moul/x/daily/closestguess/v1

Overview

Realm Path
gno.land/r/moul/x/daily/closestguess/v1
Exported Functions
3
State Entries
8
Source Files
3
Total Package Entries
19

Exported Functions

3 exported functions

State

8 state entries

Source Code

FILES
closestguess.gno
go
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/moul/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}
251

Raw Package Data

Raw JSON data