1// Package numguess is a Number Guessing Game realm.2//3// A hidden target in 1..100 is derived deterministically from the block height4// at which the round started. Players call Guess(n) to receive "higher",5// "lower" or "correct". When a round is solved the winner is recorded on the6// leaderboard (ranked by fewest guesses) and anyone can start a fresh round.7package numguess89import (10 "strconv"11 "strings"1213 "chain"14 "chain/runtime"1516 "gno.land/p/nt/avl/v0"17)1819// round holds the mutable state of the active game round.20type round struct {21 number int // 1-based round counter22 startHeight int64 // block height the round began at (entropy source)23 target int // hidden value in 1..10024 solved bool // set once someone guesses correctly25 lastHint string // "higher" / "lower" / "correct" / "" for none yet26 winner string // address of the solver, once solved27 winnerTries int // attempts the winner needed28}2930// winRecord is one leaderboard entry.31type winRecord struct {32 addr string33 round int34 tries int35 target int36}3738var (39 current *round40 // attempts counts guesses per address for the CURRENT round only.41 attempts = avl.NewTree() // addr(string) -> *int42 // leaderboard is append-only across all rounds; sorted on render.43 leaderboard []winRecord44)4546func init() {47 current = newRound(1, runtime.ChainHeight())48}4950// newRound builds a fresh round whose target derives deterministically from51// the start height. Gno has no runtime RNG, so height is the only entropy.52func newRound(number int, height int64) *round {53 return &round{54 number: number,55 startHeight: height,56 target: targetFromHeight(height),57 solved: false,58 lastHint: "",59 }60}6162// targetFromHeight maps a block height to a hidden number in 1..100.63// Deterministic and cheap: a small integer mix so consecutive rounds don't64// land on obviously adjacent targets.65func targetFromHeight(h int64) int {66 if h < 0 {67 h = -h68 }69 mixed := (h*2654435761 + 12345)70 if mixed < 0 {71 mixed = -mixed72 }73 return int(mixed%100) + 174}7576// Guess submits a guess for the current round. It records the caller's attempt77// count and returns "higher", "lower" or "correct". Crossing function: caller78// invokes as Guess(cross(cur), n).79func Guess(cur realm, n int) string {80 if !cur.IsCurrent() {81 panic("spoofed realm")82 }83 if n < 1 || n > 100 {84 panic("guess must be in 1..100")85 }86 if current.solved {87 panic("round already solved; call NewRound to start a fresh one")88 }8990 caller := cur.Previous().Address().String()91 incAttempt(caller)9293 var hint string94 switch {95 case n < current.target:96 hint = "higher"97 case n > current.target:98 hint = "lower"99 default:100 hint = "correct"101 current.solved = true102 current.winner = caller103 current.winnerTries = attemptsOf(caller)104 leaderboard = append(leaderboard, winRecord{105 addr: caller,106 round: current.number,107 tries: current.winnerTries,108 target: current.target,109 })110 chain.Emit("RoundSolved",111 "round", strconv.Itoa(current.number),112 "winner", caller,113 "tries", strconv.Itoa(current.winnerTries),114 )115 }116117 current.lastHint = hint118 return hint119}120121// NewRound resets the game with a fresh target when the current round is122// solved. Panics if the current round is still open. Crossing function.123func NewRound(cur realm) string {124 if !cur.IsCurrent() {125 panic("spoofed realm")126 }127 if !current.solved {128 panic("current round is not solved yet")129 }130131 next := current.number + 1132 current = newRound(next, runtime.ChainHeight())133 attempts = avl.NewTree() // per-round attempt counters reset134 chain.Emit("NewRound", "round", strconv.Itoa(next))135 return "round " + strconv.Itoa(next) + " started"136}137138// incAttempt bumps the caller's attempt counter for the current round.139func incAttempt(addr string) {140 if v := attempts.Get(addr); v != nil {141 p := v.(*int)142 *p++143 return144 }145 one := 1146 attempts.Set(addr, &one)147}148149// attemptsOf returns how many guesses addr has made this round.150func attemptsOf(addr string) int {151 if v := attempts.Get(addr); v != nil {152 return *(v.(*int))153 }154 return 0155}156157// totalAttempts sums all attempts made in the current round.158func totalAttempts() int {159 total := 0160 attempts.Iterate("", "", func(_ string, v any) bool {161 total += *(v.(*int))162 return false163 })164 return total165}166167// Render produces the gnoweb Markdown view of the current game state.168func Render(path string) string {169 var b strings.Builder170171 b.WriteString("# Number Guessing Game\n\n")172 b.WriteString("Guess the hidden number between **1** and **100**. ")173 b.WriteString("Each round's target is derived deterministically from the block height at which it started.\n\n")174175 b.WriteString("## Current round\n\n")176 b.WriteString("- Round: **" + strconv.Itoa(current.number) + "**\n")177 b.WriteString("- Started at block height: " + strconv.FormatInt(current.startHeight, 10) + "\n")178 b.WriteString("- Attempts so far (this round): " + strconv.Itoa(totalAttempts()) + "\n")179180 hint := current.lastHint181 if hint == "" {182 hint = "_no guesses yet_"183 } else {184 hint = "`" + hint + "`"185 }186 b.WriteString("- Last hint: " + hint + "\n")187188 if current.solved {189 b.WriteString("- Status: **solved** by `" + current.winner + "` in " +190 strconv.Itoa(current.winnerTries) + " guess(es). ")191 b.WriteString("Call `NewRound()` to play again.\n")192 } else {193 b.WriteString("- Status: **open** — call `Guess(n)` to play.\n")194 }195 b.WriteString("\n")196197 b.WriteString("## Leaderboard\n\n")198 b.WriteString(renderLeaderboard())199200 b.WriteString("\n## How to play\n\n")201 b.WriteString("```\n")202 b.WriteString("Guess(n) // n in 1..100 -> \"higher\" | \"lower\" | \"correct\"\n")203 b.WriteString("NewRound() // start a fresh round once the current one is solved\n")204 b.WriteString("```\n")205206 return b.String()207}208209// renderLeaderboard formats winners ranked by fewest guesses (ties: earlier210// round first). Sorted on a copy so state is untouched.211func renderLeaderboard() string {212 if len(leaderboard) == 0 {213 return "_No winners yet — be the first!_\n"214 }215216 ranked := make([]winRecord, len(leaderboard))217 copy(ranked, leaderboard)218 sortByFewestTries(ranked)219220 var b strings.Builder221 b.WriteString("| Rank | Player | Round | Guesses | Target |\n")222 b.WriteString("|---|---|---|---|---|\n")223 for i, w := range ranked {224 b.WriteString("| " + strconv.Itoa(i+1) + " | `" + w.addr + "` | " +225 strconv.Itoa(w.round) + " | " + strconv.Itoa(w.tries) + " | " +226 strconv.Itoa(w.target) + " |\n")227 }228 return b.String()229}230231// sortByFewestTries does an in-place insertion sort: fewer tries first, then232// earlier round first on ties. Small n; insertion sort keeps it deterministic233// and dependency-free.234func sortByFewestTries(rs []winRecord) {235 for i := 1; i < len(rs); i++ {236 j := i237 for j > 0 && less(rs[j], rs[j-1]) {238 rs[j], rs[j-1] = rs[j-1], rs[j]239 j--240 }241 }242}243244func less(a, b winRecord) bool {245 if a.tries != b.tries {246 return a.tries < b.tries247 }248 return a.round < b.round249}250Guess(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
NewRound(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
Render(path 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.