Building a Wordle Solver With Python

Jack C
CodeX
Published in
10 min readFeb 9, 2022

With a Solve Rate of Over 99%!

Given that the last thing I wrote about was creating a Sudoku solver with Python, and that my Wordle score had taken a beating over the past few days — I landed on the idea of building a ‘Wordle bot’ to see if it could be beaten.

You can follow along any code in my notebook here, I’m planning on doing a follow-up where I move this to a Streamlit app (a Python library used for building simple apps) — if I do so then I’ll move the code into Python files.

So what’s Wordle?

For the uninitiated, Wordle is the free-to-play browser based game that has taken social media by storm since it’s launch last October.

It even got acquired for a 7 figure sum by none other than the New York Times.

Wordle becoming more popular than COVID, source: Google trends

The game itself is simple enough. You get 6 attempts to guess a 5 letter word, and for each attempt the letters you guess are coloured as following:

  • Green: right letter, right place
  • Yellow: right letter, wrong place
  • No colour: wrong letter

An example game might look like this:

Guessing the word in 4 turns, source

As you can imagine, there are a lot of possibilities for different 5 letter words — but there has to be some logic that can help us out here.

Sourcing the data

The first step was to get a hold of the list of possible 5 letter words that could be used as answers.

Fortunately, after a bit of digging and stumbling on this gem on stackexchange (the puzzling subsection, no less), I found out that the Wordle app itself uses 2 lists of words:

  • A list of 2,315 possible 5 letter answer words
  • A list of 10,657 possible 5 letter reference words (none are used for answers, but they can be used as guesses)

The logic behind the 2 lists seems to be that the former is a list of more “common” words that are more reasonable to expect people to guess.

The 10,657 list is the remainder of the ‘valid’ 5 letter words that were excluded from being answers.

Either way, you can find these 2 lists here. For this, we’re unsurprisingly going to be using the 2,315 word list as our list of potential guesses (the list var La).

A strategy

Photo by Markus Winkler on Unsplash

Before we go coding up our game and a strategy, we need a plan. Guessing words at random isn’t that helpful, so what should our strategy be?

We want to guess words that eliminate as many possible wrong answers. We can do that by counting letters.

[As I mentioned at the start, you can follow along all of the code in this article with my notebook — or just follow the screenshots]

Let’s start by taking a look at the words in our possible answer set, and identify the letters in each position.

Image by author

To eliminate as many wrong letters in position 1, we’d want to know how often letters occur in column 1 — and choose the most common one in our guess.

Image by author

So for position 1, the letter ‘A’ occurs 141 times and ‘B’ occurs 173 times. So a word beginning with the letter ‘B’ would be a better guess than ‘A’ for narrowing down position 1.

To eliminate as many wrong answers as possible with a guess, you want to choose a word that eliminates as many possible letters across all positions.

To do this, our strategy is to count the number of letters in each position — and choose the word with the highest total frequency of letters occurring across all positions.

The best initial guess for Wordle using the above logic is ‘SLATE’, with a combined frequency of 1,437 across the 5 positions.

This approach isn’t perfect, as it would double count frequently co-occurring letters (e.g. positions 4&5 being “ER” — the frequency of “E” in 4 and “R” in 5 would both count separately towards our frequency). But, as we’ll see later, this approach is good enough.

Coding part 1: The Game() class

Enough planning. Now we can get coding!

Firstly we want to be able to create a game using a Python class.

For those unfamiliar with classes — they’re a cornerstone of Object Oriented Programming (OOP) in Python. At a very high level, they are “things” that can have properties (attributes) and functions (methods).

As an example — a “Dog” class in Python could have height/colour/weight attributes, and a bark() method.

For our use case, we want to be able to create a Game class.

So what should our Game class contain?

If we take a step away from this being a Python problem, we want to know these things at any given time in our Wordle game:

  • Which letters we’d guessed correctly, and in what positions
  • Which letters in the alphabet we can still guess with (after eliminating any from incorrect guesses)
  • Which letters are misplaced (correct letter, wrong location — highlighted yellow)
  • What possible 5 letter answers remain
  • The frequency of letters in each of the 5 positions, for the remaining possible 5 letter answers (to get our guesses)

We can do this through our constructor function, or __init__ as it’s known. This is just a fancy function that sets attributes (properties) when we create an object using our Game class.

Image by author

We can then create our class simply by calling MyGame = Game(df_words_5l) (using our dataframe created earlier).

If you give this a go, then try MyGame.possible_letters, you should see a list of all of the letters in the alphabet as we are yet to eliminate any!

The functions

As I mentioned before, I’ll just be sharing screenshots here but you can see any of the source code in this notebook.

If you remember from our strategy section above, we need to be able to calculate a ‘frequency score’ for each word based on how many letters are in each of the 5 positions within our dataframe of possible words.

Image by author

Now we can score words! We can use this to then make our guesses.

Image by author

The top row of this dataframe can be used as our guess word each turn, as it has the highest frequency score.

Filtering our list of possible words for those containing correct letters (e.g. ‘S’ in position 1) is simple. Filtering for those not containing incorrect letters is also simple. Filtering for misplaced letters, however, is far trickier.

Image by author

This function relies on us first filtering the dataframe of possible answers for correct answers, and for a misplaced letter — filtering out rows where the letter appears in the misplaced spot. Hopefully the documentation makes this clear!

Now, the last bit of the puzzle for our Game class is how to update the attributed based on a guess, and the results.

The below code is from the update function which takes arguments of:

  • guess: 5 letter string
  • results: list in the format [N,N,N,N,N] where 0 is incorrect, 1 is misplaced, and 2 is correct with respect to our guess

This first chunk handles our inputs, and then deals with correct answers.

Image by author

For correct answers, we want to remove any previously misplaced letters that have now been guessed correctly (e.g. ‘E’ in position 3 was yellow in guess 1, and green in position 2 in guess 2). We also want to filter our dataframe of possible answers.

The second chunk is a bit trickier. Misplaced letters need:

  • The possible answer dataframe filtered for words that contain the misplaced letter in the misplaced position
  • Counting to handle cases where we may have >1 of the same misplaced letter (e.g. 2 misplaced ‘E’s)!
Image by author

numpy.vectorize runs functions on dataframes much faster than the standard dataframe.apply (learnt that the hard way).

Remember that our check_misplaced_letters function is simply checking that the remaining words in the dataframe contain at least the same number of misplaced letters.

Our last chunk of this function deals with incorrect letters, removes them from the list of possible letters to guess from*, and then filters our dataframe of possible answers accordingly.

Image by author

*One caveat is that a letter can be ‘incorrect’ but still part of our list of valid letters.

If the answer was ‘PEARL’, and our guess was ‘ENTER’, the first ‘E’ in our guess would show up as yellow (misplaced) but the second would not be highlighted at all (incorrect).

This only states that there aren’t 2 E’s in the word, not that E is an incorrect letter! So we have to be careful when filtering down our list of possible letters.

A quick test

Before we go about building something that can play the game for us, let’s have a go at using our new class on today’s (9th Feb 2022) Wordle.

Image by author

Not bad! Although being British I’m not sure I agree with the spelling of humour..

Proof!

Coding part 2: The game playing function

Now we have a game! To check that the above wasn’t a fluke we want something that can:

  • Create an instance of Game() for us (an “instance” is just when you set something up, e.g. MyGame = Game(dataframe))
  • Take a target word and evaluate a guess against that target word, giving us a list of results
  • Feed the results back into our Game() instance, and calculate our next guess
  • Loop through the above until all letters are correct (result = [2,2,2,2,2]), or 6 guesses have passed

The next sections of code are taken from the play_game function in this notebook that’s been linked throughout the article. This function takes arguments of target_word, df_possible_words (used to construct our Game class), and debug which I’ll go into later.

We start by initialising the game, and making a guess. We then check the correct letters first as these are the easiest to handle.

Image by author

The next piece of code in the function looks at what letters (positions) remain. We can break the loop here if all no positions remain (correct guess), or continue to play.

We then check the remaining target letters, and check whether any of our guess letters appear in the target word at least once.

Let’s say we guessed ‘THING’, and the target was ‘CHART’. ‘H’ is correct, so we exclude position 2, leaving ‘T_ING’ AND ‘C_ART’. We’d want to tag the ‘T’ in our guess with a 1, and the rest as 0s.

Image by author

The reason for some of this complexity is due to double letters. If we guessed ‘GREET’, and the target was ‘STAGE’, we’d want to tag the first ‘E’ as misplaced (1) but the second as incorrect (0).

I’ll leave the final part of the function out of here as it’s largely debugging or output related which is better shown by it’s use.

Setting debug=True prints the guess and the result at each turn, whereas setting debug=False instead returns 2 results after the game finishes (correct guess or 6 turns pass) — the target word and the number of turns taken.

Let’s give it a whirl with debug=True.

So far so good

However, to really prod at this approach we want to test it on all 2,315 possible answers and see how it fares. This is a job for debug=False.

Looping through all words

I imagine that using dataframes probably slows this down. There are probably more efficient ways of running my code, but 2 minutes for a one-off isn’t bad!

Image by author

Good news — we take an average of just over 3.5 turns to solve any Wordle puzzle!

Bad news — we have 13 words that beat our tool.

The ones that got away

After spending some time digging through these step by step it just seems that these are unfortunate edge cases that our tool doesn’t handle.

I imagine there’s a way to improve this — maybe excluding or down weighting past Wordle answers may help — but a hit rate of 2302/2315 (99.4%) is pretty good!

Conclusion

We were able to build something pretty successful!

If you have any ideas on how to beat the last 13 or any other improvements, please let me know.

As next steps, I’d potentially like to build this out into a Streamlit app to make it a bit more interactive.

All code from the above was taken from this notebook. Thanks for reading!

--

--

Jack C
CodeX
Writer for

I write about Data Analytics and Analytics Engineering