1// Package wordle is a daily five-letter word puzzle realm for gno.land.2//3// The day's secret word is picked deterministically from the current block4// height (a "day index"), so every player faces the same word for the same5// span of blocks. Players submit guesses with Guess; each guess is scored6// letter-by-letter with emoji feedback (🟩 correct spot, 🟨 wrong spot,7// ⬜ absent). Render shows the caller's board, attempts left, and rules.8package wordle910import (11 "chain/runtime"12 "chain/runtime/unsafe"13 "strings"14)1516// maxAttempts is the number of guesses a player gets per day.17const maxAttempts = 61819// wordLen is the fixed length of every word / guess.20const wordLen = 52122// blocksPerDay controls how many blocks share the same secret word. Testnet23// blocks are fast, so this is a pragmatic "day" for a demo puzzle rather than24// a wall-clock day. Deterministic: derived only from block height.25const blocksPerDay = 172802627// words is the built-in dictionary of candidate secret words. All entries are28// exactly wordLen lowercase letters.29var words = []string{30 "crane",31 "slate",32 "pride",33 "ghost",34 "lunar",35 "mango",36 "vivid",37 "quilt",38 "brave",39 "flame",40 "storm",41 "pearl",42 "trout",43 "zebra",44 "cabin",45}4647// board holds one player's guesses for a given day.48type board struct {49 day int6450 guesses []string // raw lowercase guessed words, in order51 won bool52}5354// players maps a caller address string to that player's current board.55// A plain map is fine here: keys are addresses, iteration order is never56// used for rendering (we only ever look up the caller's own board).57var players = map[string]*board{}5859// dayIndex returns the deterministic day index for a given block height.60func dayIndex(height int64) int64 {61 if height < 0 {62 height = 063 }64 return height / blocksPerDay65}6667// wordForDay returns the secret word for the given day index. Deterministic:68// a pure function of the day index and the built-in dictionary.69func wordForDay(day int64) string {70 n := int64(len(words))71 idx := day % n72 if idx < 0 {73 idx += n74 }75 return words[idx]76}7778// currentDay returns today's day index from the chain height.79func currentDay() int64 {80 return dayIndex(runtime.ChainHeight())81}8283// score compares a guess against the secret and returns wordLen emoji tiles:84// 🟩 right letter right spot, 🟨 right letter wrong spot, ⬜ letter absent.85// Standard Wordle two-pass scoring so duplicate letters are handled correctly.86func score(guess, secret string) string {87 g := []rune(guess)88 s := []rune(secret)8990 tiles := make([]string, wordLen)91 // counts of each secret letter still available to match as 🟨.92 counts := map[rune]int{}9394 // First pass: greens.95 for i := 0; i < wordLen; i++ {96 if g[i] == s[i] {97 tiles[i] = "🟩"98 } else {99 counts[s[i]]++100 }101 }102 // Second pass: yellows / greys.103 for i := 0; i < wordLen; i++ {104 if tiles[i] == "🟩" {105 continue106 }107 if counts[g[i]] > 0 {108 tiles[i] = "🟨"109 counts[g[i]]--110 } else {111 tiles[i] = "⬜"112 }113 }114 return strings.Join(tiles, "")115}116117// isFiveLetters reports whether w is exactly wordLen ASCII lowercase letters.118func isFiveLetters(w string) bool {119 if len(w) != wordLen {120 return false121 }122 for i := 0; i < len(w); i++ {123 c := w[i]124 if c < 'a' || c > 'z' {125 return false126 }127 }128 return true129}130131// Guess records a five-letter guess for the caller against today's word.132//133// It is a crossing (state-mutating) function: callers invoke it as134// Guess(cross(cur), "crane"). Identity is taken from the live crossing frame135// after the IsCurrent authentication check.136func Guess(cur realm, word string) {137 if !cur.IsCurrent() {138 panic("spoofed realm")139 }140 caller := cur.Previous().Address().String()141142 word = strings.ToLower(strings.TrimSpace(word))143 if !isFiveLetters(word) {144 panic("guess must be exactly 5 lowercase letters a-z")145 }146147 day := currentDay()148 b := players[caller]149 if b == nil || b.day != day {150 // New player, or a new day rolled over: reset the board.151 b = &board{day: day}152 players[caller] = b153 }154155 if b.won {156 panic("you already solved today's word")157 }158 if len(b.guesses) >= maxAttempts {159 panic("no attempts left today")160 }161162 b.guesses = append(b.guesses, word)163 if word == wordForDay(day) {164 b.won = true165 }166}167168// Render returns the caller's board as Markdown. It is NOT a crossing function169// (no cur realm parameter). It reads the viewer via the stack-walking170// unsafe.PreviousRealm(); an unknown viewer simply sees an empty board.171func Render(path string) string {172 viewer := unsafe.PreviousRealm().Address().String()173 day := currentDay()174175 var sb strings.Builder176 sb.WriteString("# 🟩 Daily Word Puzzle\n\n")177 sb.WriteString("A Wordle-like game. Everyone gets the same secret 5-letter word each ")178 sb.WriteString("\"day\" (a span of blocks). You have ")179 sb.WriteString(itoa(maxAttempts))180 sb.WriteString(" attempts.\n\n")181182 sb.WriteString("**Day #")183 sb.WriteString(i64toa(day))184 sb.WriteString("**\n\n")185186 b := players[viewer]187 if b == nil || b.day != day {188 sb.WriteString("_No guesses yet today._\n\n")189 writeRules(&sb)190 return sb.String()191 }192193 secret := wordForDay(day)194 sb.WriteString("## Your board\n\n")195 for _, g := range b.guesses {196 sb.WriteString(score(g, secret))197 sb.WriteString(" `")198 sb.WriteString(strings.ToUpper(g))199 sb.WriteString("`\n\n")200 }201202 left := maxAttempts - len(b.guesses)203 switch {204 case b.won:205 sb.WriteString("🎉 **You solved it!** Come back next day for a new word.\n\n")206 case left <= 0:207 sb.WriteString("💀 **Out of attempts.** The word was `")208 sb.WriteString(strings.ToUpper(secret))209 sb.WriteString("`. Try again next day.\n\n")210 default:211 sb.WriteString("**Attempts left:** ")212 sb.WriteString(itoa(left))213 sb.WriteString("\n\n")214 }215216 writeRules(&sb)217 return sb.String()218}219220// writeRules appends the legend / how-to-play block.221func writeRules(sb *strings.Builder) {222 sb.WriteString("## Rules\n\n")223 sb.WriteString("- Guess the 5-letter word in ")224 sb.WriteString(itoa(maxAttempts))225 sb.WriteString(" tries: `Guess(\"crane\")`.\n")226 sb.WriteString("- 🟩 correct letter, correct spot.\n")227 sb.WriteString("- 🟨 correct letter, wrong spot.\n")228 sb.WriteString("- ⬜ letter not in the word.\n")229 sb.WriteString("- The word changes every day (deterministic from block height).\n")230}231232// itoa formats a small non-negative int (attempt counts) without importing233// strconv, keeping the dependency surface minimal.234func itoa(n int) string {235 return i64toa(int64(n))236}237238// i64toa formats an int64 as decimal.239func i64toa(n int64) string {240 if n == 0 {241 return "0"242 }243 neg := n < 0244 if neg {245 n = -n246 }247 var digits []byte248 for n > 0 {249 digits = append(digits, byte('0'+n%10))250 n /= 10251 }252 // reverse253 for i, j := 0, len(digits)-1; i < j; i, j = i+1, j-1 {254 digits[i], digits[j] = digits[j], digits[i]255 }256 if neg {257 return "-" + string(digits)258 }259 return string(digits)260}261Guess(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}, word 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.