Solving Wordle: Part II

Nesh Patel
7 min readMay 8, 2022

--

Last time, after much grovelling, we introduced Bernard to the world of Wordle. It wasn’t long until he had mastered the game, but one thing remains: to actually play the real game on the New York Times website, because what good is mastery of a petty frivolity without the ability to gloat to all and sundry about it?

When Bernard conquered the world of Sudoku, was forced to learn how to read a newspaper which was extremely challenging for our plucky machine. Fortunately, this time the game is implemented on a website which is literally speaking his language. Of course I’m using the word literally quite figuratively: the website is written in JavaScript and Bernard speaks Python. But it’s still a lot easier than before, largely because of the translator-cum-butler Selenium who is able to accept instructions from a ruthless overlord and diligently execute them on a browser. This exemplary servant only asks one thing of us, that our requests are articulated clearly and in full without ambiguity.

This snippet will open up a browser in incognito mode (where we can ignore those pesky cookies):

Figure 5: Wordle cookie popups on the NYT site

The first thing we need to do is close those popups. Using Chrome, right click and choose inspect to view the Document Object Model (DOM). The DOM is a virtual representation of the objects on a web page. In the very early days of the internet, the DOM was essentially identical to the HTML of the page. The modern web is a lot more dynamic, with the DOM being constantly generated and manipulated by sophisticated web applications. We can interact with it too using the Chrome developer tools as well as JavaScript and of course Selenium.

In the Chrome inspector, we can see the reject button has the catchy name pz-gdpr-btn-reject. Let’s get Selenium to click this for us:

Our loyal servant is employing a clever little trick here. We can tell him to wait until an element is definitely clickable, lest he act in an overzealous manner and risk alienating some guests with a faux pas:

WebDriverWait(self.driver, timeout).until(
expected_conditions.element_to_be_clickable(element)
)

expected_conditions contain some common functions which are useful, but we can also define our own: more on this later.

The next thing to close is the help screen, as Bernard already knows all of the rules. There’s an buttons with a class called close-iconunder the game-modalthat looks perfect:

nyt_web.driver.find_element(By.CSS_SELECTOR, 'game-modal .close-icon')

However when we run this, we get this rather ugly error:

Message: no such element: Unable to locate element: {"method":"css selector","selector":"game-modal .close-icon"}

This is strange, because the element is clearly there:

Figure 6: Close Icon for the help modal

If we look at the DOM in Chrome we can see that a few elements above our button is something called #shadow-root. After spending too much time reading about sophisticated hair dye techniques, I realised this wasn’t referring to follicle manipulation at all. It is an encapsulation technique for elements on a webpage. By design, nothing inside the shadow DOM can access anything outside it. Luckily, our esteemed butler can be told to plunge into these murky depths and fetch items of our choosing:

Wordle provides an online keyboard so we can use the same technique to enter words. Once we have that, it’s easy for our butler to fetch information from the page:

Figure 7: Evaluation attributes for guesses

The rest is a trivial matter of telling Bernard the hints, and then relaying his expert guesses to our butler who will enter them in the site for him. For those following along, there’s a few functions left for you to implement in the play function:

And so , Bernard is finally able to fully accomplish his goal, no doubt earning the adoration of his followers:

However, on accomplishing this feat, he was not as pleased as I was expecting. It seems that although the majority of fans were indeed impressed by his abilities, there were a growing minority that scoffed at the triviality of it all. These self-described intellectual behemoths didn’t bother themselves with such tawdry games as Wordle. No, they had evolved past such a childish game and were now busying themselves with loftier ambitions.

Dordle, Quordle and Octordle are how they sated their cranial appetites. Not satisfied with one game of Wordle, these twists on the game have multiple words running in parallel:

Figure 8: Dordle # 96 with guesses: CRANE, PIOUS, HAUTE, DAUBY, FABLE and LAUGH

Bernard sat dejected, having worked so hard to solve this game and earn back his fame and fortune it all appeared it was for nothing. But perhaps it was not all for nothing. The technique that he used to solve a single game of Wordle, can be extended to solve many. The best move of a single game is the move with the highest expected value. So it stands to reason that the best move across multiple games, is the one with the highest sum of values from each game.

The game code is easy enough to tweak to support playing multiple words at once. Now we can test out this theory, playing a Quordle with a maximum of 9 guesses and the words (chosen at random) MUSKY, CIVIL, FAUNA and TOOTH:

ANSWERS = ['MUSKY', 'CIVIL', 'FAUNA', 'TOOTH']
bot_play_multi(
num_games=4,
max_guesses=9,
answers=ANSWERS,
)
CRANE CRANE CRANE CRANE
x x x x x ● x x x x x x ○ ● x x x x x x
TOILS TOILS TOILS TOILS
x x x x ○ x x ○ ○ x x x x x x ● ● x x x
TOUGH TOUGH TOUGH TOUGH
x x ○ x x x x x x x x x ● x x ● ● x x ●
DUSKY DUSKY DUSKY DUSKY
x ● ● ● ● x x x x x x ○ x x x x x x x x
MUSKY MUSKY MUSKY MUSKY
● ● ● ● ● x x x x x x ○ x x x x x x x x
CIVIL CIVIL CIVIL
● ● ● ● ● x x x x x x x x x x
FAUNA FAUNA
● ● ● ● ● x x x x x
TOOTH
● ● ● ● ●

Amazing, and this scales to games of 8 and 16 words as well as beyond. One thing though is that playing these multi-games is a lot slower than previously. The code above takes ~10 seconds to execute which may as well be an eternity. Enter: multiprocessing (pause for applause).

You might have heard that running code in parallel is not possible in Python due to something called the Global Interpreter Lock (GIL). There’s some truth to this statement, and it’s worth reading the article to understand what’s going on. But ultimately we can run Python functions in parallel, even when they hold the GIL. The multiprocessing library allows you to spin up subprocesses on different CPU cores, which side steps the GIL problem. You can think of it as running a slow Python function on one terminal, then opening up another terminal and running another one.

It’s worth talking a bit about multiprocessing’s cousin, threading, because the two often get confused by American tourists, often leading to embarrassing results. The main difference is that multiprocessingis good at CPU-bound tasks and threading is good at I/O bound tasks. This refers to whether the task mostly uses CPU, like our Wordle solver, or mostly uses input/output resources, like fetching data from a URL. Another major difference is that threads share the same memory as the main program whilst sub processes do not. This makes it a bit more costly to communicate between processes than between threads.

So with the theory out of the way, I’ll demonstrate with an example:

We can compare the performance between the two modes:

Starting Serial...
TOILS
Time taken: 9.92s

Starting Parallel...
TOILS
Time taken: 4.95s

That’s about a 50% reduction in the time taken. It’s worth noting that there’s a cost spinning up sub processes and collecting the results across them, so the performance improvement is only really felt on the longest running processes. In my final version, I’m only using this technique for the first move we have to assess (which is always the most expensive).

Now with a bit of work in the Chrome inspector we can make sure Bernard communicates with our butler in the proper manner. Once complete, the results speak for themselves:

And there we have it. Bernard can now add Wordle (and it’s various spin-offs) to the collection of games he has conquered. Unlike Sudoku, Wordle is actually a really fun game to play, so we won’t be spoiling this one for everyone. But I do hope at least some of you have enjoyed learning a touch about the mathematics and computation that allows us to solve problems like these, and that you feel encouraged to go out and build solutions to problems of your own devising.

Acknowledgements

1Blue3Brown and his lesson on information theory using Wordle. I’m yet to publish a blog that doesn’t reference his wonderful work. His video is already the most famous solution to this problem, and I wanted this blog to be a compliment to it, exploring teaching the programming in a bit more detail.

Josh Wardle for creating Wordle, a genuinely fun game that I’m glad has exploded in popularity.

My friend Sunny, for reading all my rough drafts and crummy jokes and helping me to craft them better instead of simply mocking me mercilessly.

My loving proof-reader and wife, for her patience, encouragement and for not filing for divorce whilst I was working on this project.

Appendix

GitHub Project: Full solution and code, for those following along and want to see the finished article.

--

--