Programmatically solving every Wordle answer

The approach I’ve used to build a Wordle-solver

Dan Palmer
10 min readFeb 17, 2022

Recently I’ve taken it upon myself to build a Wordle solver — purely as an exercise to test my recently acquired Go knowledge. Here’s how it went.

Photo by Joshua Hoehne on Unsplash

Problem

I’m sure I don’t need to go into how Wordle works, but the very brief summary is:

  • You have to guess the 5-letter answer
  • Each time you guess, you are told if you have the right letter in the right place (green), the right letter in the wrong place (yellow) or a wrong letter (black)
  • You have 6 guesses

So the question is, what strategy should be used to solve this?

Considerations

To determine my approach, I considered the following:

  1. I’m ignoring “hard-mode”, just the standard game rules.
  2. I can use the real answer list.
  3. I don’t have infinite resources, so I don’t want test every combination of guesses against every answer and see which yields the more successful results, this isn’t going to scale up well and I find it more interesting to find a good balance between speed and accuracy. I set myself the goal if executing a full game of Wordle in 50 milliseconds, calculating my next guess in real-time based on the latest knowledge from the previous guesses.
  4. The list of possible answers is not the same as the list of possible guesses — Wordle maintains two separate lists (which you can view if you look at the javascript source of the site, but be warned this will spoil your enjoyment of the game as the lists are in order of when the answers will be used in the game) and I can use guesses that I know will never be an answer to narrow down the answer list.
  5. There is little value in using a letter when you already know the result (this is why “hard mode” is harder — it restricts your ability to test more letters at once).

And finally, something I’m not considering…

The Wordle answer list is curated, it’s been vetted to exclude any odd/obscure words that would be too hard to guess. I’m ignoring this fact and just taking each word at face value. I’m not ingraining any understanding of the English language to assist the logic (such as common letter patterns).

My approach could just as easily solve “ziram” (a word you are allowed to guess but will not be an answer) as it could “river” (which is a real answer).

My approach

My application follows this logical flow:

  1. Find the “best” guess from the possible answer list
  2. Test the “best” guess against the answer — if it’s an exact match then finish here
  3. Apply the result of the test to narrow down the possible answer list, and go back to #1

Finding the “best” guess

I decided that the “best” guess is one where each letter I use comes close to halving the possible answers. This means I’m minimising the risk of wasting a guess, and because I have 5 letters I could potentially cut the list down to 0.5⁵ (or 3%) of the original size assuming every letter appears in exactly half the answers.

Guessing low-frequency letters might give me a few lucky hits, but in all likelihood, I’m only excluding a very small percentage of words by guessing a ‘J’ (1.1% of Wordle answers contain a J).

So, first I count how often each letter of the alphabet appears in the list of possible answers. From the original 2309 possible answers, the letter frequency is:

  a=906  b=266  c=446  d=370  e=1053 f=206
g=299 h=377 i=646 j=27 k=202 l=645
m=298 n=548 o=672 p=345 q=29 r=835
s=617 t=667 u=456 v=148 w=193 x=37
y=416 z=35

So “e” appears in 1053 of the 2309 answers, whereas “j” appears in just 27.

From this count, I score each possible guess (note this is the possible GUESSES, not the possible ANSWERS — see consideration #4) based on how closely it appears in 50% of the words (1154), and going over 50% is just as bad as being under.

This gives the following scores for each letter where 1.0 means the letter appears in exactly 50% of letters an 0.0 means the letter appears in either none or every answer:

  a=0.78  b=0.23  c=0.39  d=0.32  e=0.91  f=0.18
g=0.26 h=0.33 i=0.56 j=0.02 k=0.17 l=0.56
m=0.26 n=0.47 o=0.58 p=0.30 q=0.03 r=0.72
s=0.53 t=0.58 u=0.39 v=0.13 w=0.17 x=0.03
y=0.36 z=0.03

From this scoring, I can find the “best” word by giving every word I could guess a score. There’s less value in guessing a word with a double letter, so I only give unique letters a score to simply my logic.

An example would be the word “guess” which has a score of 2.09, made up of:

Scoring 'guess':
g=0.26 u=0.39 e=0.91 s=0.53
total=2.09

By evaluating every possible word, I can find the word with the highest score.

Testing my best guess against the answer

I can now apply my best guess against the real answer, by either playing the Wordle game online or running some of my own code to simulate Wordle.

Either way, the outcome will be 3 pieces of information:

  1. Letters which appear in the answer and I know the position they are in (green letters)
  2. Letters which appear in the answer and I know the position they are not in (yellow letters)
  3. Letters that do not appear in the answer (black letters)

I combine these with my knowledge from previous guesses to make a full list of each piece of information.

Applying the results to the answer list

To apply my results to the possible answer list, I iterate over the full list and exclude a word if it meets any of these criteria:

  1. Any green letters do not match (info #1)
  2. It does not contain all the yellow letters (info #2)
  3. It contains a yellow letter in a previously guessed position (info #2)
  4. It contains any black letters (info #3)

From here, I have a smaller answer list, and can repeat the entire process. This time the answer list will be different so contain different letters, letter scores will be different, and the resulting best guess will be different.

Exceptions

There are 2 exceptions to simply repeating this process:

  1. If my answer list is down to 2, I can just guess one of the words — I might as well try to get a match in 1 guess here as the worst case is still 2 guesses. It would take 2 guesses if I were to use a 3rd word to exclude 1 of the remaining answers anyway.
  2. If my answer list is all anagrams of each other (e.g. pleat, plate, petal, leapt) — in this case, every letter of the alphabet occurs in 0% or 100% of the answers, so there is no “best” guess. Here I just need to guess one of the possible answers to get some green letters.

Worked example

Suppose the answer is “green”…

Iteration 0 (no guess)
Possible guesses: 10638
Possible answers: 2309
Letter scores:
a=0.78 b=0.23 c=0.39 d=0.32 e=0.91 f=0.18
g=0.26 h=0.33 i=0.56 j=0.02 k=0.17 l=0.56
m=0.26 n=0.47 o=0.58 p=0.30 q=0.03 r=0.72
s=0.53 t=0.58 u=0.39 v=0.13 w=0.17 x=0.03
y=0.36 z=0.03
Best guesses:
oater, score: 3.58
orate, score: 3.58
roate, score: 3.58
realo, score: 3.56
retia, score: 3.56
Iteration 1 (guess = "oater")
Possible guesses: 10638
Possible answers: 42
Letter scores:
a=0.00 b=0.14 c=0.38 d=0.57 e=0.00 f=0.29
g=0.19 h=0.05 i=0.71 j=0.00 k=0.05 l=0.29
m=0.00 n=0.29 o=0.00 p=0.48 q=0.00 r=0.00
s=0.38 t=0.00 u=0.38 v=0.10 w=0.14 x=0.00
y=0.05 z=0.00
Best words to guess:
cupid, score: 2.52
pudic, score: 2.52
scudi, score: 2.43
ludic, score: 2.33
nidus, score: 2.33
Iteration 2 (guess = "cupid")
Possible guesses: 10638
Possible answers: 7
Letter scores:
a=0.00 b=0.29 c=0.00 d=0.00 e=0.00 f=0.57
g=0.29 h=0.29 i=0.00 j=0.00 k=0.00 l=0.57
m=0.00 n=0.57 o=0.00 p=0.00 q=0.00 r=0.00
s=0.29 t=0.00 u=0.00 v=0.29 w=0.57 x=0.00
y=0.00 z=0.00
Best words to guess:
flawn, score: 2.29
fawns, score: 2.00
flans, score: 2.00
flaws, score: 2.00
lowns, score: 2.00
Iteration 3 (guess = "flawn")
Possible guesses: 10638
Possible answers: 1
Letter scores:
N/A
Best words to guess:
green
Iteration 4 (guess = "green")
Solved "green"

Performance

My implementation of this approach has the following execution times:

  • Determining the best guess (when there are ~2,000) possible answers: 10–12 milliseconds
  • Determining the best guess (when there are ~12,000) possible answers: 11–13 milliseconds
  • Solving 1 Wordle game (determining the guess, guessing, filtering the list, repeat): 35–40 milliseconds
  • Solving every (2309) Wordle game: 58 seconds

Note that this is without any real focus on performance, I have not considered memory allocation, concurrent processing, etc, just basic good practices with my loops and sorting algorithms.

Summary of findings

I tested my application on every possible Wordle answer, and found that all were solved in 6 guesses, and most in 4:

Distribution of the number of guesses to solve all Wordle answers

None of the answers were done in 1 guess, because my logic determines that the best word to start with is not a Wordle answer.

The best opening guess is “oater” (or an anagram such as “roate” or “orate”).

The hardest answers to solve contain double letters, or use low-frequency letters. The answers that take 6 guesses to solve with this approach are:

ferry jolly polyp glaze terra jazzy gamma chili leery drier mammy couch state shall bobby fecal jerky wound

Most of these contain a double letter, or a J or Z.

I occasionally get some deviation from this in my results due to the way I pick from the list on those exceptional cases, so occasionally a word moves from 5 to 6 guesses and vice versa.

Earlier I mentioned one of my considerations was that I knew the official Wordle answer list so could create accurate letter scores to aid my guessing. If I adjust my word lists so that in theory any English word (I’m using a list of ~12,000) could be the answer then this reduces the accuracy of my letter scores and my results become:

Distribution of the number of guesses to solve all Wordle answers (when the known answers are not used to refine the guesses)

As you can see, the guesses are shifted slightly to the right with about 1% of Wordle answers not being solved within 6 guesses.

Using this ~12,000 word list also adds about 50% on to the execution time compared to the list of 2,309.

Improvements

There are plenty of improvements I could make. Here’s a few initial ideas:

  1. Consider the position of letters when determining the best guess. As well as using the frequency of a letter, then consider in which position is it most useful to apply it. Getting a green “e” is going to narrow the list down more than getting a yellow “e” for example, and it might be better to play a letter which frequently occurs in the same position in that position over a letter which occurs more frequently but in different positions.
  2. Consider the relationship between letters. Some letters are more likely to occur with other letters so guessing them both might not be the most efficient way of using the letters. For example, “NIQAB” allows you to check the answer for “Q” without having to check for “U” which you would be 99% certain would be there anyway.

Sample Code

Here’s a few samples from by code, most of it is trivial looping through lists, sorting lists, filtering lists etc, but here’s the pertinent code to my “best” guess logic.

Counting letter frequency

Go through each word, mark which letters appear in that word, then increment the overall letter count for any letters that appear. I don’t increment every time I see a letter because it would double-count duplicate letters.

matchingWords []string := ... // An array of all the possible matching answers
// Assume we're dealing with just a-z
letterCount := make([]int, 26)
// Go through each possible answer word
for _, word := range matchingWords {
// Mark the letters that appear in that word (this ensures we don't could double letters twice)
letterAppears := make([]bool, 26)
for i := 0; i < len(word); i++ {
letterAppears[word[i]-'a'] = true
}
// Increment the overall count for any letter that's appeared in this word
for i := range letterAppears {
if letterAppears[i] {
letterCount[i] = letterCount[i] + 1
}
}
}

Scoring letters

Go through each letter of the alphabet, and score it based on how close it come to appearing in 50% of letters.

e.g. 1000 words, if ‘e’ appears in 600 words then the calculation is:

  • Find the the frequency of the letter: 600/1000 = 0.6
  • Find how far that is off the ideal frequency: abs(50% — 60%) = 0.1
  • Invert it, as 0% is the best and 50% is the best: 0.4
  • Double it so we use 1.0 as the perfect score rather than 0.4 (just because it makes it cleaner to understand, the order wouldn’t change): 0.8
matchingWords []string := ... // An array of all the possible matching answers
letterCount []int := ... // An array containing the count of all letters in the matching words
// Assume we're dealing with just a-z
letterScores := make([]float64, 26)
// Give each letter a score based on how close it is from 0.5 frequency
for i, c := range letterCount {
letterScores[i] = 2 * (0.5 - math.Abs(0.5-(float64(c)/float64(len(matchingWords)))))
}

Scoring each word

Score each word, but use a map so we only record 1 score per unique letter.

word string := .... // the word to score
letterScores []float64 := ... // An array containing the score of each letter
// Score each letter (once)
wordLetterScore := map[int]float64{}
for _, c := range word {
s := wordLetterScore[int(c-'a')]
if s <= 0 {
wordLetterScore[int(c-'a')] = letterScores[int(c-'a')]
}
}

// Add up the total score
score := 0.0
for _, s := range wordLetterScore {
score = score + s
}

Alternative Approaches

Depending on your aim (e.g. every answer within 6, or the quickest solution, or performance), there are plenty of other approaches that could be taken. Here’s a few:

  1. Brute force — run every possible combination of guesses against every possible answer to determine which combinations find the answer first and which is the most common first guess in those successful combinations.
  2. Letter-elimination — in the first 5 guesses, try and use 25 different letters. This will let you know which letters are in the answer and which are not (bar 1 letter). Unless you are unlucky enough for the answer to be an anagram of another possible answer and you didn’t hit on any green letters, then solving the answer on the 6th guess should be trivial. An example 5 guesses with unique letters from Wordle’s allowed guess list are: XYLIC, WAQFS, VOZHD, BRUNG, KEMPT.

--

--