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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

wordle.gno

5.72 Kb · 191 lines
  1package home
  2
  3import (
  4	"net/url"
  5	"strconv"
  6	"strings"
  7	"time"
  8)
  9
 10// Gnordle: guess the day's five-letter word in six tries. The game keeps no
 11// state on chain. Past guesses ride in the page path, wordle/<n>/CRANE.SLATE,
 12// and a gnoweb form adds the next one as ?guess=, so playing needs no wallet.
 13
 14const (
 15	wordLen    = 5
 16	maxGuesses = 6
 17	launchDay  = 20719 // 2026-09-23, in days since 1970-01-01 UTC: puzzle #1
 18	stride     = 17    // coprime with len(words), so every word comes up once per cycle
 19)
 20
 21// words are the answers, gno and blockchain flavoured.
 22var words = []string{
 23	"REALM", "CROSS", "BLOCK", "CHAIN", "TOKEN", "STAKE", "PANIC", "DEFER",
 24	"SLICE", "ARRAY", "PRINT", "STACK", "QUEUE", "TUPLE", "FLOAT", "CONST",
 25	"PARSE", "PROOF", "NONCE", "VOTER", "SHARD", "MERGE", "AUDIT", "DEBUG",
 26	"TRACE", "CACHE", "INDEX", "QUERY", "ROUTE", "VALUE", "RUNES", "ERROR",
 27	"GUARD", "OWNER", "ADMIN", "TREES", "NODES", "PEERS", "GRAPH", "FORKS",
 28	"MINTS", "LOGIC", "BYTES", "CLASS", "FIELD", "LOOPS", "PROBE", "SCOPE",
 29	"TYPED", "BENCH", "CLONE", "PATCH", "EMBED", "FETCH", "LABEL", "LOCAL",
 30	"PROXY", "SCALE", "SPAWN",
 31}
 32
 33// today returns the number of the current puzzle, from the last block's time.
 34func today() int {
 35	return int(time.Now().Unix()/86400) - launchDay + 1
 36}
 37
 38// answer returns the word of puzzle n.
 39func answer(n int) string {
 40	i := ((n-1)*stride%len(words) + len(words)) % len(words)
 41	return words[i]
 42}
 43
 44// score colours a guess against the answer, handling repeated letters the way
 45// the original game does: a letter is marked present only as many times as it
 46// is still unmatched in the answer.
 47func score(guess, ans string) string {
 48	marks := make([]string, wordLen)
 49	left := map[byte]int{}
 50	for i := 0; i < wordLen; i++ {
 51		if guess[i] == ans[i] {
 52			marks[i] = "🟩"
 53		} else {
 54			left[ans[i]]++
 55		}
 56	}
 57	for i := 0; i < wordLen; i++ {
 58		if marks[i] != "" {
 59			continue
 60		}
 61		if left[guess[i]] > 0 {
 62			marks[i] = "🟨"
 63			left[guess[i]]--
 64		} else {
 65			marks[i] = "⬛"
 66		}
 67	}
 68	return strings.Join(marks, "")
 69}
 70
 71func validGuess(g string) bool {
 72	if len(g) != wordLen {
 73		return false
 74	}
 75	for i := 0; i < wordLen; i++ {
 76		if g[i] < 'A' || g[i] > 'Z' {
 77			return false
 78		}
 79	}
 80	return true
 81}
 82
 83func renderWordle(path string) string {
 84	path, query, _ := strings.Cut(path, "?")
 85	parts := strings.Split(path, "/") // "wordle", [n], [guesses]
 86	n := today()
 87	var guesses []string
 88	if len(parts) > 1 {
 89		v, err := strconv.Atoi(parts[1])
 90		if err != nil || v < 1 {
 91			return wordleError("There is no puzzle with that number.")
 92		}
 93		n = v
 94	}
 95	if n > today() {
 96		return wordleError("Puzzle #" + strconv.Itoa(n) + " is not out yet.")
 97	}
 98	if len(parts) > 2 && parts[2] != "" {
 99		guesses = strings.Split(parts[2], ".")
100	}
101	if len(parts) > 3 || len(guesses) > maxGuesses {
102		return wordleError("This game link is broken.")
103	}
104	for _, g := range guesses {
105		if !validGuess(g) {
106			return wordleError("This game link is broken.")
107		}
108	}
109
110	ans := answer(n)
111	notice := ""
112	if q, err := url.ParseQuery(query); err == nil && q.Get("guess") != "" && !solved(guesses, ans) && len(guesses) < maxGuesses {
113		g := strings.ToUpper(strings.TrimSpace(q.Get("guess")))
114		if validGuess(g) {
115			guesses = append(guesses, g)
116		} else {
117			// The guess is never echoed: it comes from the link, and markdown in
118			// it would render on this page under the owner's address.
119			notice = "> That guess is not five letters, A to Z.\n\n"
120		}
121	}
122
123	num := strconv.Itoa(n)
124	var b strings.Builder
125	b.WriteString("# Gnordle #" + num + "\n\n")
126	b.WriteString("Guess the word in six tries. 🟩 right letter, right place. 🟨 in the word, elsewhere. ⬛ not in the word. ")
127	b.WriteString("Everyone gets the same word, and a new one comes out every day at 00:00 UTC.\n\n")
128	b.WriteString(notice)
129
130	grid := ""
131	for _, g := range guesses {
132		row := score(g, ans)
133		grid += row + "\n"
134		b.WriteString(row + " " + g + "\n\n")
135	}
136	if miss := missing(guesses, ans); miss != "" {
137		b.WriteString("Not in the word: " + miss + "\n\n")
138	}
139
140	switch {
141	case solved(guesses, ans):
142		b.WriteString("**Solved in " + strconv.Itoa(len(guesses)) + "/6.** Copy your result:\n\n")
143		b.WriteString("```\nGnordle #" + num + " " + strconv.Itoa(len(guesses)) + "/6\n" + grid + "```\n\n")
144	case len(guesses) == maxGuesses:
145		b.WriteString("**The word was " + ans + ".** Copy your result:\n\n")
146		b.WriteString("```\nGnordle #" + num + " X/6\n" + grid + "```\n\n")
147	default:
148		b.WriteString("<gno-form path=\"wordle/" + num)
149		if len(guesses) > 0 {
150			b.WriteString("/" + strings.Join(guesses, "."))
151		}
152		b.WriteString("\">\n  <gno-input name=\"guess\" placeholder=\"Guess " +
153			strconv.Itoa(len(guesses)+1) + " of 6: five letters\" />\n</gno-form>\n\n")
154	}
155
156	b.WriteString("[Start over](" + home + ":wordle/" + num + ")")
157	if n < today() {
158		b.WriteString(" · [Play today's puzzle](" + home + ":wordle)")
159	}
160	b.WriteString(" · [Back home](" + home + ")\n")
161	return b.String()
162}
163
164func solved(guesses []string, ans string) bool {
165	return len(guesses) > 0 && guesses[len(guesses)-1] == ans
166}
167
168// missing lists the guessed letters that are not in the answer, in guess order.
169func missing(guesses []string, ans string) string {
170	var out []string
171	seen := map[byte]bool{}
172	for _, g := range guesses {
173		for i := 0; i < wordLen; i++ {
174			c := g[i]
175			if !seen[c] && !strings.ContainsRune(ans, rune(c)) {
176				out = append(out, string(c))
177			}
178			seen[c] = true
179		}
180	}
181	return strings.Join(out, " ")
182}
183
184func wordleError(msg string) string {
185	return "# Gnordle\n\n" + msg + "\n\n[Play today's puzzle](" + home + ":wordle) · [Back home](" + home + ")\n"
186}
187
188func renderGameTeaser() string {
189	return "## Gnordle\n\nGuess today's five-letter word in six tries. Same word for everyone, " +
190		"a new one every day, no wallet needed. [Play puzzle #" + strconv.Itoa(today()) + "](" + home + ":wordle)\n"
191}