⬜🟨🟩: Wordle & Absurdle Internals

Ed0
14 min readFeb 6, 2022

--

Over the past few months, my social media feed has been filled with posts such as:

Wordle 231 3/6
🟨⬜🟨⬜⬜
🟩🟩🟩⬜⬜
🟩🟩🟩🟩🟩

It’s kind of annoying. Perhaps you’ve experienced the same on your own platform of choice. I never thought I’d find myself yearning for photos of my friend’s dogs, babies, and houseplants, but at this point I’d take anything over another post filled with little square emoji. It seems like everyone I know has now fully adopted a daily ritual of: wake, play Wordle, post smug Wordle grade sheet to social media. It’s a weird choice of fixation.

Now, I happen to know a thing or two about being fixated on puzzles. Back in 2014, in what I have since come to value as an excellent practice run for the lockdowns of the COVID-19 pandemic, I was living at home with my parents while I figured out how to cope with the chronic fatigue that’d caused me to drop out of grad school. I spent the best part of two years essentially bed-bound, getting up only to use the bathroom, eat, and eventually, do short walks around the block, or meet with a friend in the park. During this time, I found myself entirely fixated on a type of puzzle known as a Hanjie or Crosspuzz, which is a bit like a Sudoku. (If you’d like to give them a try, I can recommend Simon Tatham’s puzzle collection, which simply refers to this type of puzzle as “Pattern.”)

I’d do Hanjie puzzles for hours each day, partly to help myself stay awake, and partly to fight off the boredom that accompanied not being able to do very much while awake. As I slowly gained more physical stamina over the two years, I found that, strangely, even when I needed to be doing something else — and had the physical capacity to do so — I’d find myself yearning to be solving a Hanjie instead. It was very clear that I’d become a bit of an addict.

In the heat of my obsession, I sat down and wrote a Python script that could automatically solve any tractable Hanjie puzzle. I still have the script in a private repository in my Github; the code is embarrassingly clunky. Despite this, the important takeaway for me was that once I’d written the solver, I suddenly found no joy in solving a Hanjie puzzle by hand. What was the point? I had a script that could do it for me. Almost overnight, I forgot about Hanjie puzzles, and got on with sorting my life out.

So, fast forward to 2022. My friends are obsessed with a one-page mastermind-style game that’s just been bought by the New York Times for several million dollars, and it’s getting to be somewhat of a drag. Suddenly, my once-niche (okay, it’s still very niche) piece of life experience started to feel very relevant again. Would it be possible to write an automatic solver for Wordle, give it to all my friends, and convince them to post dog photos to my feed once again?

Absurdle 0: What’s Absurdle?

Because regular Wordle looked fairly straightforward, I actually started out looking at a variant called Absurdle, written by qntm. Absurdle is an adversarial version of Wordle: it contains logic that responds to your guesses by shifting between the entries in a set of possible overall solutions, with the goal of keeping you guessing for as long as possible. It stood out to me as an interesting problem to solve, and I figured that I’d come back to regular Wordle afterwards. (I did.)

What follows is a description of Absurdle’s internal logic, along with a couple of approaches to writing an automated solution for it.

Absurdle 1: How does Absurdle work?

Before actually looking under the hood at Absurdle, I’d expected to perhaps find some Javascript that’d send a request to the server each time the user entered a guess. The response to this request would contain information about the user’s guess, and this information would then be displayed on the page. This arrangement would have been fairly robust, but would present a challenge to any attempt to reimplement Absurdle locally, since the list of valid words and possible solutions would both be stored on the server and thus hidden from my view. I’d have to either use qntm’s page as a sort of oracle, which would mean sending loads of requests to it (bad), or else generate my own local list of valid guesses and answers by writing some sort of black-box querying algorithm to get the server to incrementally reveal its secrets (hard, slow, and bad). Thankfully, neither of these approaches were necessary.

It turns out that the entirety of Absurdle is implemented lovingly in pure Javascript. When a user loads the page, they download a huge list of 12928 five-letter words that the game considers to be valid guesses, of which only 2312 are used as solutions to the puzzle. The user’s guesses are then validated locally by the Javascript running in the page. qntm has left a lot of really insightful (and beautifully formatted) console output in the page too, which was incredibly useful for understanding the logic used by Absurdle when determining its response to each of the user’s guesses.

Beautiful console output, with emoji!

My first port of call was to take apart the Javascript and understand how Absurdle worked. In broad strokes, this is the logic used by the game:

  • The game starts with a pool of 2312 possible valid solutions.
  • Each time the user enters a guess, the game starts by checking whether that guess exists in its list of 12928 possible valid five-letter words. If it doesn’t, the word is rejected, and if it does, the game proceeds.
  • The game then enumerates all the possible ways it could respond to the user’s guess (for example, 🟨🟩🟩⬜⬜, ⬜🟨🟨⬜🟩, 🟩⬜⬜⬜🟩 , and so on). There are exactly 3⁵ = 243 possible responses.
  • For each response, the game considers how many solutions in the pool would be consistent with that response to the user’s guess. (For example, if the user were to guess AIERY, the solutions ADORE, AWARE, and AZURE would be consistent with a response of 🟩⬜🟨🟩⬜, but CRYPT, IRONY, and LEAKY would not.)
  • The game selects the response that is consistent with the greatest number of solutions in the pool.
  • Finally, the game displays this response to the user. The new pool becomes the pool of solutions that are consistent with this guess/response combination, and the game is now ready to receive another user guess.

The game’s strategy at each step is to maintain the largest possible pool of solutions that are consistent with each of the user’s guesses. The user’s strategy, therefore, is to submit guesses that shrink the pool of solutions as much as possible at each step, until only one valid solution remains.

This is a slightly simplified description of what the game is doing: in the case that there exists more than one response which would leave the largest possible pool of consistent solutions, Absurdle scores each response and chooses the one with the lowest score. The logic used to compare two responses is as follows:

  • The response with the smallest number of 🟩 has the lowest score.
  • If tied, the response with the smallest number of 🟨 has the lowest score.
  • If tied, the two responses are compared left-to-right. For the first position in which they don’t contain the same colour square, the response containing a in that position has the lowest score. If neither contain a , the response containing a 🟨 in that position has the lowest score.
Absurdle scoring function
Python reimplementation

With all this in mind, I put together a pure Python reimplementation of Absurdle, which I’ve called Pyrdle. It contains a pre-fetched set of wordlists from Absurdle so as to avoid hammering qntm’s page (if you end up tinkering with it, please don’t add parts that result in the Absurdle page getting hammered.) To play, just run pyrdle.py — you shouldn’t have to install any python packages to get it to run.

Absurdle 2: Can we find solutions to Absurdle?

In the About section of Absurdle, we are told that the best possible score in Absurdle is 4 guesses. Can we write a procedure that will find one or more answers that solve the puzzle in this many guesses?

As a naive first approach, consider the following strategy:

  1. We start with a pool of all 2312 possible solutions.
  2. Using Pyrdle as an oracle, we check each of the 12928 possible valid guesses to see what Absurdle’s response to them would be, and how many solutions would remain in the pool after using them.
  3. We select the guess which results in the smallest possible number of solutions remaining in the pool.
  4. We submit this guess, and repeat from step 2 until we complete the puzzle.

This is an example of a greedy algorithm, which divides the problem into stages, and gets the optimal result at each stage. It also isn’t necessarily a great strategy, as although we’re finding the optimal choice at each step, we have no guarantee that these optimal choices will combine to form an optimal solution to the whole problem — it’s just kinda likely that the solution will end up being pretty good.

Pyrdle can perform this strategy for us. To use it, run pyrdle.py -s (the -s flag simply means “solve.”) You’ll see some output a bit like this:

locard@rhyme: ./pyrdle.py -s
AESIR
BLUDY
POTCH
TOUGH
MOUTH

Found the following solution in 153.754168s:
AESIR, BLUDY, POTCH, TOUGH, MOUTH

This is a complete solution to Absurdle — you can go and test it on the page! It is not, however, an optimal solution, as it requires five guesses rather than four. This is as we might expect given our approach; our greedy algorithm has found a solution that’s pretty good, but not perfect.

In order to find better solutions, we need to understand how different combinations of guesses combine to reduce the pool of consistent solutions. One way we could approach this might be by drawing a decision tree for the problem. Consider a mini version of Absurdle that only accepts single-letter guesses (A bit like an adversarial version of edjeff’s Letterle) and has a set of possible solutions that’s limited to A, B, or C. The decision tree for this game might look like this:

Decision tree for mini-Absurdle

There are six possible routes through the tree, which each arrive at one of the possible solutions to the puzzle. Six is a nice manageable number, and we could easily analyse every route in this tree before selecting the optimal solution (though in this case, all routes are optimal solutions.) For real Absurdle, however, rather than having only three possibilities for each guess, there are 12928 possible guesses. Even if we only consider paths through the tree that are four guesses in length — you’ll remember qntm’s advice that a solution is possible in four guesses — there are still nearly 12928⁴ paths, which is approximately 2.8 x 10¹⁶, or 28,000,000,000,000,000. This is quite a lot more than six, and is probably more paths than we can analyse in the time it takes for our tea to brew.

Instead of analysing every single possible path, we can try an approach that is something of a compromise between this full-search approach and our greedy algorithm. In broad strokes, after each guess, we limit our analysis to a small number of promising-looking paths through the tree, and discount the rest. Here’s our strategy:

  1. We start with a pool of all 2312 possible solutions. We also choose the number of paths to which we’d like to limit our analysis — we’ll call this number N. We start with exactly one path, which has no guesses.
  2. For each path we are analysing (initially one, but will be as many as N later in this algorithm) we consult Pyrdle to check what Absurdle’s response to each of the 12928 possible valid guesses would be, and how many solutions would remain in the pool after using them.
  3. Each guess produces a new path through the tree.
  4. Of these paths, we select the N paths which result in the smallest possible number of solutions remaining in the pool. These become the N paths that we’ll track.
  5. We then repeat from step 2 with our new set of N paths, and continue until at least one path reaches a solution.

Hopefully it’s clear that the amount of analysis that needs to be performed here is a lot less. Assuming this approach is able to find a solution in four guesses, We’ll need to analyse at least ((3N+1) * 12928) guesses, but for a small-ish N, this is a far more manageable amount than if we were searching the whole tree.

Pyrdle can perform this strategy for us using N=20. To use it, run pyrdle.py -s -p (the -p flag simply means “prune,” as we can call this a branch-pruning strategy.) As before, you’ll see some output that looks a bit like this:

locard@rhyme: ./pyrdle.py -s -p

Found the following solution in 433.635695s:
AIERY, CLOTS, SPAWN, QUASH

This, too, is a complete solution to Absurdle, but uses only four guesses. It is an improvement on our initial greedy algorithm! As four guesses is the optimal solution size, we’d be hard-pressed to do better than this, and can stop looking for better strategies.

… or can we? You can see above that on my (extremely janky) computer, this solve took all of about seven minutes. This is significantly longer than our greedy algorithm, which comes in at only two and a half minutes. Can we perhaps make our strategy faster?

The one parameter we might be able to tweak is N. For N=1, we’re back to our greedy algorithm. What is the smallest N between 1 and 20 which will still find a solution using four guesses?

Pyrdle can do this analysis for you. You can set the size of n using the -n flag. For example, to run with N=7, we’d run pyrdle.py -s -p -n 7. If your shell is Bash, you can iterate over runs using values 20 through to 1 by running:

for N in {20..1}; do ./pyrdle.py -s -p -n $N; done

Tabulated below are the running times for each value of N. This isn’t a very thorough test — I only measured each N once — but hopefully gives a sense that smaller N results in a shorter running time, as we’re analysing fewer paths and therefore calculating fewer guesses. Lines with a ✅ produced an optimal result using just four guesses, and lines with a ❌ didn’t, instead requiring five.

20 : 436.445534s ✅
19 : 416.319660s ✅
18 : 406.526900s ✅
17 : 392.267597s ✅
16 : 387.846636s ✅
15 : 384.071285s: ✅
14 : 377.257658s: ✅
13 : 346.668595s: ✅
12 : 344.797218s: ✅
11 : 327.962589s: ✅
10 : 320.201427s: ✅
9 : 292.961275s: ✅
8 : 273.606862s: ✅
7 : 262.496309s: ✅
6 : 245.329166s: ✅
5 : 237.600470s: ❌
4 : 230.835850s: ❌
3 : 204.337718s: ❌
2 : 179.630117s: ❌
1 : 156.974520s: ❌

At N=5, we see that the algorithm can no longer find a solution in four guesses, which makes N=6 the optimal size for N. This finds an optimal solution in a time significantly closer to that taken by the greedy algorithm: around four minutes.

Absurdle 3: Can we solve Absurdle’s challenge mode?

Absurdle also offers a “challenge mode”, in which the puzzle provides a target answer, and requires you to back it into a corner such that the solution to the puzzle becomes the provided target answer. This might initially sound confusing, but we can in fact solve this scenario with only a small modification to either of the two approaches described above. Below is an outline for a modified version of our greedy algorithm from earlier which can solve Absurdle’s challenge mode. I’ve highlighted the important modification to save you re-reading everything.

  1. We start with a pool of all 2312 possible solutions.
  2. Using Pyrdle as an oracle, we check each of the 12928 possible valid guesses to see what Absurdle’s response to them would be, and how many solutions would remain in the pool after using them.
  3. We discount any guess which results in Absurdle removing the target solution from the pool.
  4. We select the guess which results in the smallest possible number of solutions remaining in the pool.
  5. We submit this guess, and repeat from step 2 until we complete the puzzle.

You can hopefully see how a similar modification could be made to our pruning algorithm too. If you’re interested, have a look at the Pyrdle source, where I’ve tried to take a stab at doing so.

Pyrdle can solve challenge mode for you. To do so, simply give the target word as a final argument, e.g. pyrdle.py -s -p KOALA. The -p flag isn’t strictly necessary; if you’d like a faster but perhaps less optimal solution, omit the -p flag to simply use our greedy algorithm again. A brief look at the challenge mode solver may convince you that different target words may have different optimal solution sizes; I’ve yet to perform this analysis on all 2312 possible target words yet, and so it remains an open question.

Ok, but what about Wordle?

After a quick look under the hood, it appears that Wordle, like Absurdle, has its entire list of allowed solutions and possible guesses fully embedded in the client-side Javascript.

Wordle solutions list

But how does it know which one is correct? It turns out that the algorithm for this is pretty simple:

  • Calculate the number of days elapsed since the 19th of May 2021
  • Use this as an index into the list of solutions

This means that it’s straightforward to see what today’s solution is going to be, along with tomorrow’s, and the day after that. Solving Wordle is a lot simpler than finding answers to Absurdle!

As an added bonus, I rolled this up and put it inside Pyrdle. To get today’s Wordle answer, just run pyrdle.py -w (the -w flag is, of course, for Wordle). I’ve pre-fetched the wordlist so as not to unduly add to the Wordle server load, so using Pyrdle in this way won’t require network access — it’s fully offline.

But even after writing this solver: did I really achieve what I set out to do?

After sharing this work with my friends, I very quickly found out a couple of notable things:

  1. People have surprisingly strong opinions about Wordle.
  2. Pyrdle is, unsurprisingly, not the first time someone has written a solver. The Twitter account @wordlinator (now suspended) used to tweet each day’s Wordle solution, and I’ve no doubt that there are many more that have also taken a swing at this.

Whether or not the existence of Pyrdle will dissuade my friends from continuing to post Wordle reports to their social media pages remains to be seen. Maybe it’ll eventually die out. Maybe it’ll become a regular fixture on the front page of the New York Times, and we’ll be stuck with it forever. Ironically, in the course of producing this post, I’m aware that I’ve spent countless hours plugged into the two puzzles, and have now probably posted more words on the subject than any one of my friends — which likely means I’ve become the very person I was hoping to remediate.

I should point out that nothing here could really be considered hacking. Wordle sends the solution to your computer along with the puzzle — all these scripts do is read that solution from the files it sends you. Absurdle doesn’t have one solution, but many, and again, these scripts simply read those solutions from the files it sends you, and reasons about them given the guesses you provide to it.

In any case, I hope you enjoy the scripts (here’s a link again) and have fun extending them to your word game of choice. Just, please, whatever you do: don’t post your results to social media. Keep the dog photos coming instead. ✣

--

--