In my previous post. I explained how you can code the NY Times puzzle game Wordle in Python. I recommend reading the previous post (Coding Wordle) if you are unfamiliar with how Wordle works.
My sister is a fan of this puzzle and told me she could only play it once a day on the NY times site, so I coded it. However having played it a few times it turns out I am terrible at Wordle.
This gives me two options:
- Work on my spelling and vocabulary (something i’ve actively avoided my whole life)
- Cheat
Yup… time to cheat.
How can we Cheat?
I want to write a computer programme to tell me what the most likely words are within the puzzle. We will need to code a few functions for this.
- A function to score words – a higher score the better the word
- A function to remove words which are no longer in scope
- A method to display possible words for a user to choose
- A way in which the user can input the word they typed into the puzzle and the output of the puzzle
What Makes a Good Word?
The first problem we need to consider is what makes a word a good choice in Wordle. Having thought about this a little bit I think the following 3 factors need to be considered.
Maximise Distinct Letters
we want to be able to have as many distinct letters in the guess as possible in order to maximise our chances of both matching a letter but also increasing the number of letters we can potentially eliminate.
words like SHOOK, FIZZY and BLESS are less preferable than words without repeating letters.
Consider Letter Frequency
There are some letters that appear more in the English language than others. for example the letters X,Q and Z are nowhere near as common as other letters. put simply based on probabilities these letters are not going to be that likely to be in the random word selected.
Maximise Vowels in Early Guesses
Every word has to contain a vowel, in our early guesses we should prefer words containing multiple vowels to establish at least one right answer early on in the game. words such as AROSE (3 distinct vowels) are particularly useful for this.
Coding the Helper
As shown in the previous post I have a text file with commonly used five letter words in it. It doesn’t include obscure words because I don’t think the Worldle answer is ever likely to be a truly obscure word as that wouldn’t exactly lead to a fun game.
What the code will do is propose the best words that can be used at each stage of the game, receive the feedback from the game on this selection and then revaluate what words are still possible to be the answer ranking them.
Letter Frequency
We know we will need to evaluate the frequency of letters in our selection function. So before we can write that function we need a way of calculating how common each letter is in our list of 5 character words.
in this context words represents our collection of common five letter words. the chain.from_interable is a function which splits each word down into individual letters. So the word Queen becomes [Q,U,E,E,N]. After this we use the massively helpful Counter function – which counts how frequent each item is within the collection given to it.
letter count therefore contains values such as this;
Letter | Count |
A | 1115 |
B | 505 |
… | .. |
Z | 39 |
this gives us the frequency of each letter in the words we will be using, however what I actually want is a percentage of the letter in relation to the total number of letters. That is what the letterFreq variable will contain. An example of this is shown below.
Letter | % of Total Letters |
A | 0.089 (8%) |
B | 0.040 (4%) |
… | |
Z | 0.003 (0.3%) |
once I have this variable set I can write a function that scores a word. ultimately there will be a selection function that will pick the “best word” to use out of the possible choices, but to do that we need to give each word a score
Calculating Word Score
This function receives 3 values;
- A word to score
- the frequency percentage table from the previous step
- the turn number of the game
The function itself is pretty simple – it initialises a starting score of 0 and a list of vowels. for each character in the word passed to it it will add the frequency percentage to the score, if the turn is one of the first 2 turns it will add a 5% of the frequency score as a bonus – thereby increasing the priority of words with lots of vowels in the early game.
it then returns this score multiplied by the number of distinct letters within the word (the set function makes the characters distinct). this scoring function therefore considers all three points raised earlier.
On turn one each word from our data file will be passed to this function and then only the top 15 words will be presented to the user as good options to enter into the puzzle. One will be selected and typed into the Worldle puzzle the results of which then need to be fed back to the programme so that it can filter to words that still remain valid, the top 15 scoring worlds will then be shown from this subset and so on until the game has finished via a win or possibly a loss (more than 6 guesses).
Evaluating Answers from the Puzzle
Now we have a way of ranking words, we now need a way of removing words based on feedback from the puzzle. Each turn we will reduce our population of potential words and then run the scoring on that subset.
firstly lets set up a system to represent the colours shown as feedback from the Wordle puzzle.
Communicating Feedback
the puzzle has 3 outcomes for a letter:
- Green – the letter guessed is in the same position in the answer e.g. If I guess QUEEN and the answer is QUAKE. the letters Q and U will be shown as green.
- Yellow – The letter guessed is shown as yellow if the letter exists in the answer, but not in the position we have put it. So for example If I If I guess QUEEN and the answer is QUAKE. Both E’s will be yellow – as E exists, but not in the 3rd or 4th positions.
- Grey – the letter is shown as grey if it doesnt exist in the answer. In our example N does not appear in QUAKE and therefore would be grey.
I will represent “green” with the number 2, “Yellow” with the number 1 and “Grey” with the number 0.
So if we guessed QUEEN and the answer is QUAKE. The string representing the outcome of this guess would be 22110.
Filtering Possible Outcomes
We need a system which keeps track of what letters can be in each position of the word. I’ve named these wordVectors. At the start of the game each position can contain any letter in the alphabet;
Our guesses will remove invalid letters from the positions. Lets Go back to our QUEEN Example. We know the first 2 letters in our guess are exactly correct. That would lead to this change;
We also know that the letter E is contained somewhere in the word but that it is NOT contained in position 3 and 4. we there for could remove “E” from those positions.
In addition to this we know that the letter N is not contained at all in the answer – and this can be removed from all columns.
the wordVector object will do exactly this, it is an array containing arrays. On initialisation it contains 5 values each one identical representing every letter in the alphabet.
we now need a function to manipulate these vectors and apply them to the list of possible words to filter the possible results.
as we loop through all the values in our response from the puzzle we look if they are 2, 1 or 0. if they are 2 we set the vector to that letter alone (the QU) if they are 1 we remove that value from this position as it is impossible. if they are 0 – we remove that value from all positions.
at the end of this function we apply the vector to the possible words list via a function named match.
The match function is shown below.
it loops through every word in the possible words list and keeps them if the matchWordVector function returns true.
the match word vector function lines up a word with the vectors collection and evaluates if there is a difference between them. For example we guessed QUEEN as our first guess and that gave us this vector;
we line up words around this vector and if a letter in the word being compared to the vector isn’t in the set it is removed from the possible words list.
the red text in the diagram above represents where a letter cannot be in that position. Therefore PAINT (starts with PA and contains N) and QUIET (an E in a position which cant be E) can be removed from the possible words list – but the answer QUAKE – is still possible.
the possible words list is now filtered to only those possible words – these words are then scored and the process repeats until victory or more than 6 turns have passed.
Additional Methods
The other methods in the code are simplistic and just allow a user to enter information via the console or display / sort the scores.
Testing the Code – Todays Wordle
Let’s see if this actually works – today is the 27th of December 2022. Lets see if we can cheat our way to completing todays NY Times Wordle.
Initial Guess
The programme shows the following as the top initial guesses;
Lets try AROSE
Not great – but now we know the word has an O in it. we can enter this pattern into the console.
The programme now recommends the following words.
lets try COLIN.
wow – we now know 2 of the letters and that the word contains an N. lets enter this into the console.
We now have the following words recommended.
Lets try COUNT
I now know by process of elimination the 3rd letter has to be an N (note that the code doesn’t know this – see the conclusion). After entering this pattern the code proposes we have 8 possible words left, but I know the third letter has to be N.
Lets try CONDO
AND WE WIN! good job I cheated really, I would have never got an Americanism!
How Well does this actually work?
So we beat todays Wordle with plenty of room to spare, but how well does this actually work?
Well to answer that we can combine what we have in this post to the game we created in the previous post, We then would be able to use code to both run and play the Wordle Game.
Refactoring the Code
I created a new file named Simulator. This contains most of the code from the two other python files in the repository, except now I don’t want a user to have to input any data. the code will always pick the top scoring word. and will feedback the results to itself.
I have moved the code into two classes. WordleHelper – a class which will “play” the puzzle. WordleGame – the game itself. the main method executes the simulation and stores the results.
Running Simulations
the code below runs the simulation.
It loops for the no of games to be played. then creates a new instance of the puzzle (a new randomly selected answer) and a new instance of the helper (a new full bank of possible words). it then repeats up to 6 times the process of selecting the top word, receiving the answer and updating the possible words list.
Logging occurs to show guesses that were made and upon the simulation completing some statistics are shown.
I ran the simulation for 1000 games and got the following results;
Here you can see the guesses of game 1000 and also the results. a 97.8% win rate!!! not bad for a days work! and certainly good enough for me to beat my sister
Analysing Failures
So why don’t you win all the time? well let’s look at a loss from this simulated run.
in game 993 we lost and the answer was PROXY. I suspect that this was due to the fact that letters X and Y have low frequency scores – and our scoring algorithm didn’t rank them highly enough to be picked, this isn’t necessarily wrong – we’ve demonstrated that the vast majority of the time we win, this was just a rare example of bad luck!
This game represents another way in which you can lose. The answer is FILES and we were pretty much on the right path from guess 3. the problem is just simply how many 5 letter words end in ILES in the English language, again we have to guess somehow and base our guess on frequency of letters but that ultimately wasn’t the right answer here.
Conclusion
Well cheating sometimes does prosper doesn’t it! I’m pretty pleased with this code and its supporting simulator. to achieve a near 98% success rate was a great result, like most things in life the effort required to improve this would be a diminishing return and chasing the last 2 percent would be a huge amount of effort for very small gains. I’m not even sure you could ever be 100% accurate in this situation as the game 964 shown above demonstrates, there is a luck aspect.
The only improvement I can easily think of goes back to the QUEEN vs QUAKE example. If we look at the representation of the vectors;
we know that position 1 is Q, we know position 2 is U, we know position 3 and 4 are not E, which means position 5 has to be E, via a process of deduction. This was also seen in todays Wordle where CONDO had to have an N as its third letter, I could deduce this – but in its current form the code code not.
Potentially we could improve our results by coding some logic around deduction, but to be honest near 98% success on a game which without the code I rarely win… well, its good enough for me.
Code is on GitHub: A Wordle Game / Wordle Solver in Python (github.com)