Boost your Wordle streak with Go

The Machine's Brain

The Wordle cracker's real brain power lies in the filter() function (called in line 37 of Listing 2) and whittling down the list of words. The main program passes in the list of words still in play, the current guess, and the evaluation score returned by the online Wordle app. The function returns the word list reduced to reflect the evaluation rules, which the main program immediately assigns again to the words-in-play list dict. Following this approach, the program keeps shrinking the word list until only the correct solution remains.

Listing 3 shows the implementation of filter(), along with a grades() function that, much like the online app, can evaluate a guess attempt against a target word. It tells the user which letters are correct, which are in the wrong place, and which do not occur at all. The grades() function returns the rating for a target word in the want parameter and for a guess in guess, containing a string of numbers such as 01201.

Listing 3

filter.go

01 package main
02
03 import (
04   "strconv"
05 )
06
07 type Grade int
08
09 const (
10   NoMatch Grade = iota
11   OtherPos
12   Match
13 )
14
15 func grades(guess, want string) string {
16   hints := make([]Grade, len(guess))
17   for i, _ := range hints {
18     hints[i] = NoMatch
19   }
20
21   wantMap := map[byte]int{}
22
23   // wanted letter counts
24   for i := 0; i < len(want); i++ {
25     wantMap[want[i]] += 1
26   }
27
28   // full matches
29   for i := 0; i < len(guess); i++ {
30     guessRune := guess[i]
31     if guessRune == want[i] {
32       hints[i] = Match
33       wantMap[guessRune] -= 1
34     }
35   }
36
37   for i := 0; i < len(guess); i++ {
38     guessRune := guess[i]
39     if hints[i] == Match {
40       continue
41     }
42     if wantMap[guessRune] > 0 {
43       hints[i] = OtherPos
44       wantMap[guessRune] -= 1
45     }
46   }
47
48   res := ""
49   for _, hint := range hints {
50     res += strconv.Itoa(int(hint))
51   }
52   return res
53 }
54
55 func filter(words []string, guess, graded string) []string {
56   res := []string{}
57
58   for _, word := range words {
59     if graded == grades(guess, word) {
60       res = append(res, word)
61     }
62   }
63
64   return res
65 }
66
67 func bestBang(words []string) string {
68   best := ""
69   count := 0
70
71   for _, word := range words {
72     runes := map[rune]bool{}
73
74     for _, rune := range word {
75       runes[rune] = true
76     }
77
78     if count == 0 || len(runes) > count {
79       count = len(runes)
80       best = word
81     }
82   }
83
84   return best
85 }

To determine the rating of a guess attempt, line 16 generates an array slice with integers whose positions match those of the letters in the guess. If there is a 2 in the corresponding position in the array slice, the letter in the guess is in the correct position, and so on. To initialize the array slice, the for loop, starting in line 17, first sets all entries to NoMatch (i.e.,  ) because of the enum-style constant in line 10, which enumerates the constants in ascending order starting at  , thanks to the keyword iota.

Line 21 then creates a hash map named wantMap that counts how many times each letter should be seen in the word. For example, if the word to be guessed is LOOSE, it assigns a value of 1 to the letters L, S, and E, and a value of 2 to the letter O. The for loop starting in line 29 then goes through all the letters in the guess and sets the corresponding hints entries to 2 if the letter exactly matches the solution in want. Each of these matches decrements the value of the total required number for this letter in wantMap by one.

Starting in line 37, the cracker program evaluates the positions that contain a letter that needs to be somewhere else. If the current position is not a direct hit, but contains a letter from the wantMap, the hints array slice will contain a value of 1 for the current position, and the counter in the wantMap will be decremented by one. If the letter appears again later in the word and its counter in wantMap is still not used up, you would find another 1 there.

When it's all said and done, grades() converts the integer array slice containing the ratings of each letter element by element into a string using the strconv.Itoa() conversion function to return it to the caller as a compact packet. The Wordle website probably uses a similar scoring algorithm.

Filtering by Score

But what does all this have to do with filtering out words from the Scrabble list that are no longer potential solutions based on the Wordle page's evaluation of the guesses? The algorithm in filter() uses the following trick to drop words that are no longer possible: It goes through the remaining list of words entry by entry, assuming in each round that the current entry is the secret solution to the Wordle puzzle. Then line 59 consults the grades() evaluator, asking it what the evaluation of the current guess word is compared to the assumed solution from the word list. Now think about this: If grades() comes back with the same rating as the algorithm from the official Wordle website (which you have because you entered it in its codified format), the assumed solution word is a genuine candidate for the solution. Otherwise, the filter can delete it – simple but effective.

To enable the main program to come up with a good suggestion from the remaining word list on the Try next prompt, the bestBang() function, starting in line 67, picks a word with as many unique letters as possible. This increases the probability that the solution will actually contain one or more of these letters and that the user can eliminate even more wrong solutions in the next step. To do this, bestBang() counts the number of different letters in each word in a hash map named runes. It remembers the word with the highest score (i.e., with the highest entropy) and returns it to the caller when done.

To compile the listings in this article, you just need to call

go build wordle.go dict.go filter.go

This gives you an executable wordle binary. Note that the cracker does not need any third-party libraries. Instead, it makes do with the pool from the Go standard library. The English Scrabble file required to run the program can be downloaded free of charge from the web [4].

Flying Start

If you want to be the first to cross the finish line, you need to get off the starting block quickly – and not only in a 100-meter race. Since Wordle's meteoric rise, scientists have thrown information theory math at the problem to determine the word that gives you a flying start in the game [5].

In general, words with many different letters turn out to be a good choice. At the same time, common letters are extremely valuable because they improve the probability of good tips returned by the Wordle scorer. This why the I started with OATER to seed the cracker.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Solving Wordle with Regexes

    Five letters, one word, six tries – that's Wordle. You can solve any Wordle in just a few steps and gain practical experience using grep and regular expressions.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News