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}