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

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}