Just exactly how many tries should one expect to solve one Wordle problem?

TL;DR: I actually don't know the exact number but I'm fairly certain that it takes roughly 4.18 tries.

NOTE: I base my modelling on the game itself with no other information - no source code peeking, no asking its devs, no nothing.

If you don't know Wordle is an online game where you guess a five-letter word. The biggest appeal is obviously the fact that it's daily - one problem per 24 hours. One can easily make an "unlimited" edition but I can clearly see how the fun collapses about such a simple game when presented without a restriction so I'd rather save my time for something else.

I myself have been participating the game for days along side with a helper script containing 8000+ words that allows me to search through words easily. One day I looked at the 3/6 score I just got and wondered what is the theoretical average score; but the math is rather tricky here because:

so I decided to Monte Carlo like every good (read: lazy) programmer does.

Finding a good dictionary

The dictionary I used for my helper script is for Scrabble which contains lots of rarely used words, or at least words that definitely isn't in Wordle's dictionary (e.g. Italy; I've tried that one before), so to get an (at least more) accurate result I have to find another one. I was surprised you can actually find one on Project Gutenberg, but it's too old to be of any use, So I used English Lexicon Project to get a (hopefully complete enough) list of words.

# requires python 3.6+
with open('Items.csv', 'r') as f:
    s = f.read()

is_word = lambda x: all('a' <= i <= 'z' for i in x)
# because of the format of the csv file i can do this little shortcut...
word_list = [eval(f'[{i}]') for i in s.strip().split('\n')]
word_list = [i[0] for i in word_list if len(i[0]) == 5 and is_word(i[0])]

This produces a list of 4612 words.

Modelling the guessing process

The guessing process is as follows:

  1. Randomly select a word as the target.
  2. Randomly select another word to check with the word selected in step 1.
  3. Filter the word list according to the result of the check.
  4. Repeat step 1~3 until I have an exact match or I run out of attempts.

Randomly selecting a word

This part is easy:

import math
import random

# `random.random` generates a number in the interval [0, 1)
select_word = lambda ss: ss[math.floor(random.random() * len(ss))]

Check a word against another word & filtering the word list

def check_word(word1, word2, word_list):
    '''
    Returns `True` if `word1` and `word2` is an exact match; otherwise
    returns the filtered `word_list`.
    '''
    if word1 == word2: return True

    for i, ch in enumerate(word1):
        if ch not in word2:
            # the current character does not exist in word2
            word_list = [w for w in word_list if ch not in w]
        elif word1[i] == word2[i]:
            # the current character exists in the same place in word2
            word_list = [w for w in word_list if w[i] == ch]
        else:
            # the current character exists in word2 but it's not in the same place
            word_list = [w for w in word_list if ch in w and w[i] != ch]
    return word_list

Repeat until having exact match

def play(word_list):
    '''
    Simulates a game. Returns the number of attemps.
    '''
    target = select_word(word_list)
    current_attempt = 0
    while True:
        check_res = check_word(select_word(word_list), target, word_list)
        current_attempt += 1
        if check_res is True:
            return current_attempt
        else:
            word_list = check_res
    return False

Note that in Wordle you can at most try 6 times but I didn't put the restriction here; this is for easy calculation in further steps.

The simulation

def simulate(n, word_list):
    '''
    Simulate `n` games with the list of words `word_list`. Returns a dict of
    number of guesses. For example a probable result could be like this:

        >>> simulate(100, ss)
        {3: 23, 4: 32, 5: 28, 6: 12, 7: 4, 2: 1}

    This means in the 100 simulated games, 23 of them ended with 3 guesses, 32
    of them ended with 4 guesses, etc..
    '''
    counter = {}
    for _ in range(n):
        play_res = play(word_list)
        if not counter.get(play_res):
            counter[play_res] = 0
        counter[play_res] += 1
    return counter

def calculate(n, word_list):
    '''
    Calculates the average of # of guesses in `n` games.
    '''
    simulate_result = simulate(n, word_list)
    result = 0
    for i in simulate_result.keys():
        n_of_games = simulate_result[i]
        result += i * (n_of_games / n)
    return result

And finally:

>>> r = [calculate(10000, ss) for _ in range(10)]
>>> r
[4.5733, 4.582299999999999, 4.589299999999998, 4.5735, 4.58, 4.5895, 4.5582, 4.5870999999999995, 4.5811, 4.571999999999999]
>>> sum(r) / len(r)
4.5786299999999995

The result seems to be roughly 4.579 tries.

Using a smaller dictionary

What if Wordle uses a much smaller dictionary, like instead of 4600+ words it's ~2500 words? Would the result be then different?

Good thing the data from English Lexicon Project comes with word frequencies so we can select the most used 2500 words:

word_list = [eval(f'[{i}]') for i in s.strip().split('\n')]
# change starts from this line:
word_list = [i for i in word_list if len(i[0]) == 5 and is_word(i[0])]
word_list = [i[0] for i in sorted(word_list, key=lambda i: i[3])[-2500:]]
>>> r = [calculate(10000, ss) for _ in range(10)]
>>> r
[4.1902, 4.1827000000000005, 4.1943, 4.1728000000000005, 4.197099999999999, 4.1675, 4.197900000000001, 4.1916, 4.1792, 4.1639]
>>> sum(r) / len(r)
4.18372

Now it's roughly 4.18 tries. This fits with our intuition: the lesser the choice the more easier it is to get the right answer.

2022.1.13