TLDR: I wrote a Wordle solver bot with Javascript and UIlicious. You can run this snippet any time, anywhere, to get your daily Wordle solution, in your browser.
Try and see if you can get a better score than the bot! Feel free to edit it and optimize the solver algorithm!
The above Wordle solver is covered in 3 parts, which covers the
UI interaction code (this article)
Wordle Statistical model, and the math behind it (@TODO)
Unit testing and benchmarking of the wordle solver (@TODO)
If you haven't already heard of Wordle, it's a fun puzzle game that challenges you to guess a valid five-letter word in six tries or less.
My wife is really addicted to Wordle, and she's been bugging me to try it every day. I suppose for her, there's a thrill in getting the right word in the least amount of tries. Since we're are both gamers and math-nerds, naturally, we talk about strategies to solve Wordle in the least number of guesses. However, when actually putting those theories to use and trying my hand at Wordle, I came to realize...
I suck at Wordle!
English isn't my strong suit per se...
I speak Javascript better.
So what better way to solve Wordle, than to write a bot to solve it for me. HA!
After a weekend of hacking I finished coding up my Wordle solver bot using JS and UIlicious, I would say it works pretty well and has managed to make the correct guess mostly within 3-4 tries so far.
It has stirred up my wife's competitive spirit and annoyed her because the bot does better on most days. Now she's not bugging me every day to play Wordle anymore.
Are you curious to find out how the Wordle solver works? Let me explain!
Basic setup
We're going to use UIlicious to interact with the Wordle website, fill in our guesses, and evaluate the hints. While UIlicious is primarily a cross-browser UI testing tool, nobody said that's all you can use it for; so why not solve some Wordle puzzles with it.
Sign up for UI-licious now
Let's set up our solver bot on UIlicious Snippets - think of it as Codepen, but for UI testing, and it is entirely free to use for executing tests in Chrome. UIlicious lets you run Javascript, so I am going to be writing the solver in JavaScript.
The first thing we'll have to do is get the bot to navigate to the Wordle site. We use the I.goTo
command to go to the official Wordle site.
I.goTo("https://www.powerlanguage.co.uk/wordle/")
After the acquisition of Wordle by the NY Times, a cookie banner is present on the page, which we need to either accept or dismiss, using the I.click
command.
I.click("Reject") // to reject cookies and close the cookie banner
Next, we will confirm the app loaded by performing a quick verification of the phrase "Guess the Wordle" using the I.see
command:
I.see("Guess the Wordle")
And dismiss the tutorial popup by using I.click
with a pixel offset to click outside the popup:
// clicks 40px down and 80px right from the top-left corner of the webpage
I.click('/html/body', 40, 80)
Now, we'll press the "Run" button to execute the browser automation script confirming, this brings us to an empty Wordle board to play with.
Next steps
Now we dive into writing more complex parts of the solver.
The bot will need to be able to:
Enter letters into Wordle
Read the hints from Wordle
Find the solution based on the hints (I'll explain this in a follow-up post)
1. Entering letters into Wordle
We have only six tries to guess the Wordle, so we will start with a simple for
loop to make 6 attempts to guess the word:
for(let r=0 ; r<6 ; ++r) {
// solver code will go in here
}
Now we are going to generate a couple of words and enter our guesses into the game. solver.suggestWord(gameState)
suggests a word based on the previous guesses and the revealed hints. I'll explain how the solver functions works in a follow-up post, stay tuned!
let guessWord = null
for(let r=0 ; r<6 ; ++r) {
// guess the next word
guessWord = solver.suggestWord(gameState)
}
After computing the guessWord
, I need the bot to enter the word into the game board, which is pretty straightforward. I use the command I.type
to enter the guess word, and I.pressEnter
to submit the guess. I also added a small wait for the game board to finish its animation and allow input for the next guess. The solver can be too fast sometimes and might attempt its next guess before the game board is ready.
let guessWord = null
for(let r=0 ; r<6 ; ++r) {
// ...
// Does some stuff to get gameState
// guess the next word
guessWord = solver.suggestWord(gameState)
// enter and submit the guess word into the Wordle game board
I.type(guessWord);
I.pressEnter();
// add a small wait for the animation to finish before entering the next guess
I.wait(2)
}
And that's it!
2. Reading the hints from Wordle
The next important step is after each guess, we need to evaluate the hints to make our next guess. This is a bit trickier because we will need to do a bit of scraping to get our hints from the HTML.
First, let's grab each row of data on the game board.
The Wordle app is entirely written using Web Components (impressive!), so we will use document.querySelector
and .shadowRoot
to dive into the <game-app>
element and grab each of the <game-row>
element which stores important information about the revealed hints.
let rowList = document.querySelector("game-app").shadowRoot. //
querySelector("game-theme-manager"). //
querySelector("#board").querySelectorAll("game-row");
Here's is what happens when we run this query in the browser console:
Each of these game-row
element has two very useful data attributes that we want:
- _letters
: shows the guess that was made for this row, e.g. "hello"
- _evaluation
: an array of hints for each letter, e.g. ["absent", "present", "correct", "absent", "absent"]
. "correct"
if the letter is in the correct position. "present"
if the letter is in the word but is in the wrong position. And "absent"
if the letter isn't in the word.
We are now going to build a state
object to store all of this information and pass it to our solver. The code below is nothing too fancy. We will wrap this in a function because we want to run it after each guess to get the updated hints.
function() {
// We prepare the state object which we would want to return
let state = { history: [], pos:[
{ hintSet:[] },{ hintSet:[] },{ hintSet:[] },{ hintSet:[] },{ hintSet:[] }
] };
// Lets get the list of result rows from the UI DOM
// unfortunately this is more complicated then needed due to shadowDoms
let rowList = document.querySelector("game-app").shadowRoot. //
querySelector("game-theme-manager"). //
querySelector("#board").querySelectorAll("game-row"); //
// Lets build the state by reading each row
for(let r=0; r<rowList.length; ++r) {
let row = rowList[r];
// a row can be blank, if there was no guess made
if( row._letters && row._letters.length > 0 ) {
let word = row._letters;
// Add it to history list
state.history.push( word );
// Iterate each char in the word
let allCorrect = true;
for(let i=0; i<5; ++i) {
let c = word[i];
if( row._evaluation[i] == "absent" ) {
// does nothing
allCorrect = false;
} else if( row._evaluation[i] == "present" ) {
state.pos[i].hintSet.push( c );
allCorrect = false;
} else if( row._evaluation[i] == "correct" ) {
state.pos[i].foundChar = c;
} else {
throw "Unexpected row evaluation : "+row._evaluation[i];
}
}
// Configure the final "allCorrect" state
state.allCorrect = allCorrect;
}
}
// And return the state obj
return state;
}
This piece of code above needs to be executed by the browser, so I need to tell the Wordle bot (which runs on a sandboxed NodeJS instance) to run the function in the browser using UI.execute
.
function getWordleStateFromUI(){
return UI.execute(function(){
// put the above code snippet in here to get the game state
})
}
We want to use the following function to get the game state after each guess, so let us go back to our for
loop and update it:
let guessWord = null;
for(let r=0; r<6; ++r) {
// get the game state
let gameState = getWordleStateFromUI();
// if we got all the letters correct, we've won, hurray!
// exit the loop in that case
if( gameState.allCorrect ) {
break;
}
// guess the next word
guessWord = solver.suggestWord(gameState);
// enter and submit the guess word into the Wordle game board
I.type(guessWord);
I.pressEnter();
// add a small wait for the animation to finish before entering the next guess
I.wait(2);
}
Whew! Now the bot can enter its guesses into the board and get the revealed hints. Next, we can work on writing the algorithm to solve the Wordle in six attempts or less!
3. Finding the solution based on the hints... in a follow-up post!
Writing the solver algorithm is the biggest challenge and a whole lot of fun. I spent quite a bit of time reading up on optimal strategies (there are papers written about this) and tweaking the algorithm. But this deserves a post on its own, so I'm going to write a follow-up post after, stay tuned!
Putting everything together
Finally, let's put together the wordle solver:
Before the start of the for
loop, we'll initialize the solver with:
const solver = new WordleSolvingAlgo(fullWordList, filteredWordList);
And after we exit the for
loop, we want to query the game state again. At this point, we've either guessed the correct word in less than 6 attempts or have made 6 guesses but don't know what's the final outcome. We will get the final game state one more time, and report the outcome using TEST.log.pass
if the bot won, or TEST.log.fail
if the bot lost the game.
// this comes after exiting the for loop...
// get the final game state
let gameState = getWordleStateFromUI();
// is it all correct?
if( gameState.allCorrect ) {
// CONGRATS!!!
I.see("NEXT WORDLE");
TEST.log.pass("==== END OF GAME ====")
TEST.log.pass("## The wordle is : "+guessWord.toUpperCase())
} else {
// DARN! Try better next time!
TEST.log.fail("==== END OF GAME ====")
TEST.log.fail("## The last guess was : "+guessWord.toUpperCase())
}
Here's what everything looks altogether (except for the solver algo part!)
// Open Wordle!
I.goTo("https://www.powerlanguage.co.uk/wordle/")
// to reject cookies and close the cookie banner
I.click("Reject")
// Is Wordle loaded?
I.see("Guess the Wordle")
// Solve Wordle!
runTheWordleSolver()
//----------------------------------
// This function runs the wordle solver,
// which will make up to 6 attempts to guess the word
function runTheWordleSolver() {
// initialise the solver
// stay tune for more about the solving algorithm!
const solver = new WordleSolvingAlgo(fullWordList, filteredWordList)
let guessWord = null;
// make up to 6 guesses
for(let r=0; r<6; ++r) {
// get the game state (includes the revealed hints)
let gameState = getWordleStateFromUI();
// if we got all the letters correct, we've won, hurray!
// exit the loop in that case
if( gameState.allCorrect ) {
break;
}
// guess the next word
guessWord = solver.suggestWord(gameState);
// enter and submit the guess word into the Wordle game board
I.type(guessWord);
I.pressEnter();
// add a small wait for the animation to finish before entering the next guess
I.wait(2);
}
// at this point, we either guessed the correct word in less than 6 attempts, or have made 6 guesses and don't know what's the outcome yet.
// get the final game state
let gameState = getWordleStateFromUI();
// is it all correct?
if( gameState.allCorrect ) {
// CONGRATS!!!
I.see("NEXT WORDLE");
TEST.log.pass("==== END OF GAME ====")
TEST.log.pass("## The wordle is : "+guessWord.toUpperCase())
} else {
// DARN! Try better next time!
TEST.log.fail("==== END OF GAME ====")
TEST.log.fail("## The last guess was : "+guessWord.toUpperCase())
}
}
Drumroll... The Moment of Truth!
Let's smash the "Run" button and see how our Wordle bot does!
Stay tuned for part 2. I'll explain the solver engine works and the math behind it. Or if you want to dive into the full code yourself, you can check it out over here, and maybe even replace WordleSolvingAlgo
it with your own algorithm to solve Wordle: https://snippet.uilicious.com/test/public/N5qZKraAaBsAgFuSN8wxCL
Happy Wordling! ππΌπ