Using Python to Help Solve Wordle

Ever since the New York Times told us about the new modern love story last week, my incoming messages have been a collage of green and yellow squares captioned by declarations of joy or lament. At least one person has told me they’ve been experiencing The Tetris Effect but with five-letter words. We’re on the verge, alright. Hey, verge isn’t a bad word to play…

What is Wordle?

Wordle is a game where you have to guess a random five-letter word within six guesses. For every guess you make, you’re told whether each letter in your guess is in the answer. A letter that you guess that is not in the answer will have a black background, while a letter that you guess that is in the word in the correct position will have a green background, and a letter that you guess that is in the word but in the incorrect position will have a yellow background.

Wordle is surprisingly addictive, and also a fun project to build in Python because it’s relatively simple. Let’s put some code together.

Getting a Word Corpus

First, we want to get a reliable word corpus that approximates the words used in the game. Every word is five letters, and all of the words I’ve encountered thusfar are relatively common words in modern English. The NLTK package in Python has several different corpora, but we have to do some work to filter out names, slang, and uncommon words.

Let’s start with the Brown Corpus (which has text from novels, government documents, news articles, and more), filter it down to words with five alphabetic characters and cross-check it with the Unix Word Corpus (used by spell-checkers). From there, we have 1,932 unique words, each of which we can check the frequency of in the Brown Corpus. This is where analysis gets slightly subjective without knowing the true corpus used in Wordle - we want the list of words to be as interesting as possible, but also ones that people would actually guess. Let’s keep in words that were used at least four times in the corpus, or around 58% of the original word list.

Creating a Function for Wordle

Next we’ll create a function for Wordle that:

  • Creates a Wordle class that was initiated with the correct answer

  • Has a function that let us make a guess with a random word

  • Iterates through each letter of the guess, and stores whether the letter was in the word, and if it was in the correct position.

  • Stores and updates the state of the previously guessed letters.

class Wordle():

    def __init__(self, answer):
        self.answer = list(answer)
        self.set_values = [''] * len(self.answer)
        self.disregarded_letters = []
        self.letters_without_placements = {}

    def clean(self):
        for letter in list(self.letters_without_placements):
            if letter in self.set_values:
                self.letters_without_placements.pop(letter, None)

    def make_guess(self, guess, print_results=True, corpus=None):
        assert len(guess) == len(self.answer), 'Guess must have same number of words as answer!'
        if corpus:
            assert guess in corpus, 'Guess is not a word!'
        for position, value in enumerate(guess):
            if value == self.answer[position]:
                self.set_values[position] = value
            elif value in self.answer:
                if value in self.letters_without_placements:
                    if position not in self.letters_without_placements[value]:
                        self.letters_without_placements[value].append(position)
                else:
                    self.letters_without_placements[value] = [position]
            elif value not in self.disregarded_letters:
                self.disregarded_letters.append(value)
        self.clean()
        if print_results:
            print('Confirmed Values:', self.set_values)
            print('Confirmed Letters Not in Word', self.disregarded_letters)
            print('Confirmed Letters In Word, Not At Position', self.letters_without_placements)
            print('')
        return self.set_values, self.disregarded_letters, self.letters_without_placements

Creating a Function to Update the Corpus

This next function updates the corpus after every guess, so that we will only include possible answers that fit the updated criteria for future guesses.

class Corpus():

    def __init__(self, initial_corpus):
        self.corpus = initial_corpus

    def remove_letter(self, letter):
        self.corpus = [i for i in self.corpus if letter not in i]

    def filter_letter_correct_position(self, letter, position):
        if letter != '':
            self.corpus = [i for i in self.corpus if i[position] == letter]

    def filter_letter_incorrect_position(self, letter, position):
        self.corpus = [i for i in self.corpus if letter in i and i[position] != letter]

    def generate_word(self):
        return random.choice(self.corpus)

Making Guesses

Now that we’ve put together the functions to recreate Wordle, let’s try some words out! We can utilize what we’ve created to make a function that:

  • Pulls a random word from the corpus as the correct answer

  • Makes n random guesses

  • Updates the available corpus based on information from previous guesses

def play_wordle(corpus, n_guesses=6, print_results=True):
    w = Wordle(random.choice(corpus))
    c = Corpus(corpus)
    success = False
    for attempt in range(n_guesses):
        guess = c.generate_word()
        set_letters, disregarded_letters, letters_without_placements = w.make_guess(guess, print_results=print_results)
        if list(guess) == w.answer:
            success = True
            break
        for position, letter in enumerate(set_letters):
            c.filter_letter_correct_position(letter, position)
        for letter in letters_without_placements:
            for position in letters_without_placements[letter]:
                c.filter_letter_incorrect_position(letter, position)
        for letter in disregarded_letters:
            c.remove_letter(letter)
    if success:
        return success, attempt + 1
    else:
        return success,0

We can run this function 10,000 times to see what percentage of the time it guesses the correct answer, and the average number of attempts it takes to guess the correct answer.

With this method, we can guess the correct answer 98.5% of the time, in an average of 3.72 attempts per successful guess.

Making the First Guess

I take no pleasure in reporting this, but the human experience of playing Wordle is inherently different than playing as a computer - while we may be familiar with most or all of the possible words, we may get caught up in the moment and forget certain five-letter words. How can we approach this game better? One approach that’s been widely talked about is optimizing the first guess. In terms of pure numbers, the first guess will usually eliminate 90-95% of the possible words, bringing the list of possible words down from ~1,100 to a much more manageable 50 or 100 words.

We can see the effectiveness of different guesses for the first word by modifying our earlier function to input a guess for the first word rather than guessing a random word. Note that we will add the num_eliminated as an output in this function - this will tell us the number of words in the corpus that this guess eliminates.

def play_wordle_initial_guess(corpus, initial_guess, n_guesses=6, print_results=True):
    w = Wordle(random.choice(corpus))
    c = Corpus(corpus)
    success = False
    num_eliminated = 0
    for attempt in range(n_guesses):
        if attempt == 0:
            guess = initial_guess
        else:
            guess = c.generate_word()
        set_letters, disregarded_letters, letters_without_placements = w.make_guess(guess, print_results=print_results)
        if list(guess) == w.answer:
            success = True
            break
        for position, letter in enumerate(set_letters):
            c.filter_letter_correct_position(letter, position)
        for letter in letters_without_placements:
            for position in letters_without_placements[letter]:
                c.filter_letter_incorrect_position(letter, position)
        for letter in disregarded_letters:
            c.remove_letter(letter)
        if attempt == 0:
            num_eliminated = len(corpus) - len(c.corpus)
    if success:
        return success, attempt + 1, num_eliminated
    else:
        return success, 0, num_eliminated

We can then bootstrap a solution by using each word as a corpus as an initial input among 1,000 random solutions. We can then see on average, for each of the available words, the average percentage of time that initial guess solves the answer, the % of words eliminated from that initial guess on average, and the number of attempts on average it takes to solve the problem with that words as the initial guess.

Below, we see that most initial guesses will eliminate between 90-95% of the words in the corpus.

And below we see the top ten words by the percentage of words eliminated.

Using the percentage of words eliminated is kind of an abstract notion, however. What is the relationship between the percentage of words eliminated by an initial guess and the probability of success? And what is the relationship between the percentage of words eliminated by an initial guess and the average number of attempts it takes to succeed?

While the relationship between a ‘good’ first guess and the probability of solving the problem looks to be noisy - the relationship between a ‘good’ first guess and solving the problem in less attempts is extremely clear.

Making Sense

So what does this all mean? Should we just guess raise or later first every time? Well, maybe, but let’s take a look at what’s driving the success of certain words.

First, one thing we know about English is that every word will have one or several vowels.

We also know that certain letters are much more likely to appear in words than others.

“S”, “T”, and “R” are the consonants that are the most common in words. We can single out the number of unique appearances of these consonants in words, and create a heatmap showing the occurrence of success bucketed by the number of unique instances of common consonants (‘S’, ‘T’ ‘R’) and the number of unique values in a given word. Below it looks like words with two unique common consonants and two or three unique vowels solve the problem in the least number of attempts on average.

Final Recommendation

Given these constraints, we can make a final recommendation of which word someone should use first when playing Wordle. Looking at words with two unique common consonants (‘T’, ‘S’, ‘R’) and at least two unique vowels, we see that “SLATE” as an initial guess gives us the correct answer 98.7% of the time in an average of 3.43 attempts. That said, any of these words would be a terrific start to your next Wordle game.

I hope you enjoyed reading this! Check out the notebook here.

Previous
Previous

How to Create a Virtual Environment in Python