Wordle is great and you should definitely play it. I play it and love it, but I'm also a lazy, lazy man. Sometimes I fire up Wordle and my brain isn't caffeinated enough to figure out five whole letters. So, like a typical engineer, I spent far too long over-engineering an automated solution.
My first guess was to check the network tab, but there were no fetch
requests in there. I'm happy to see how lightweight everything is though!
My next thought was to check the JavaScript, and I soon found these two huge arrays of words:
The first is a list of all possible daily words, and the second (much larger) list is all valid entries. This means that the word of the day is always on the client, and is picked by using the current date as an index into the array. That's cool, but I don't want to replicate that logic (and the entire dictionary) in my automated solution.
Next I check client storage: cookies, session storage, and local storage. There I find what I'm looking for...localStorage.gameState
The solution is RIGHT THERE in localStorage
. I could grab that, parse it, and throw out the solution to the console:
const gameState = JSON.parse(localStorage.getItem("gameState"));
const { solution } = gameState;
console.log(`The solution is ${solution}, you cheater`);
If I did that, I still need to press all the individual keys and then press enter. That's seven whole things. I don't have time for seven things!
Thankfully I can use window.dispatchEvent
to send my own events, so I check the code again and spot that it's specifically listening for keydown
events.
I fire Wordle in incognito, open the console, and try mimicking an 'a' key press.
Now we're cooking with gas!
I already have the solution, so if I want to enter that as individual key presses I can just split it into an array of characters, loop over it, and create a keypress for each one:
const gameState = JSON.parse(localStorage.getItem("gameState"));
const { solution } = gameState;
solution.split('').forEach((letter) => {
window.dispatchEvent(new KeyboardEvent("keydown", { key: letter }));
});
And the result:
But that's not quite lazy enough: I want it to get the solution, enter it in, AND submit it for me. Easy enough with another key press at the end:
// ...
}
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
Ok let's give it a test toast:
So we're done, right?!
WRONG
By my count, we've used 283 characters here - do you think strings just grow on trees? We're better than that. We can DO better than that.
Enter Code Golf, where the goal is to solve a given problem using as few characters as possible. The people that are good at this are probably some sort of wizard. For example, this is a fully functional implementation of Flappy Bird in just 220 characters (past it in the browser URL bar to play):
data:text/html,<body onload=z=c.getContext`2d`,setInterval(`c.width=W=150,Y<W&&P<Y&Y<P+E|9<p?z.fillText(S++${Y=`,9,9|z.fillRect(p`}*0,Y-=--M${Y+Y},P+E,9,W),P))):p=M=Y=S=6,p=p-6||(P=S%E,W)`,E=49) onclick=M=9><canvas id=c>
The code is basically unreadable, because it's expected to use lots of tricks and nuances of the language to shorten the code which usually would be avoided as it harms readability. Does this mean that code golf creates bad habits and should be avoided by beginners? I don't think so: to get better at Code Golf you need to learn the language really well to expand your toolset, and know what methods and tricks to use in what situations. Also it makes you great at reading minified code! Maybe, I don't know, probably not.
So let's take another look at our original implementation:
const gameState = JSON.parse(localStorage.getItem("gameState"));
const { solution } = gameState;
solution.split('').forEach((letter) => {
window.dispatchEvent(new KeyboardEvent("keydown", { key: letter }));
});
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
(283 chars)
The most obvious thing that jumps out to me that we could trim is all the logic to parse gameState
as JSON. It's a plain string in localStorage
, and the part of the string that has the solution is always in the same place (on a clean game), between characters 105 and 110. So, instead of making a nice object from it, we can just pull the individual letters of the solution from the gameState
string:
for(let i = 105; i < 111; i++) {
const key = localStorage.getItem("gameState")[i];
window.dispatchEvent(new KeyboardEvent("keydown", { key }));
}
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
(219 chars)
Immediately I can spot some near duplicated code: window.dispatchEvent(new KeyboardEvent("keydown", { key: [THIS BIT IS DIFFERENT] }))
.
It's 99% the same, except for the key
. What if I created a function that I can pass a key
to, and it will dispatch a keydown
event for me? Then I can replace both instances where I'm doing this manually:
const d = (key) => window.dispatchEvent(new KeyboardEvent("keydown", { key }));
for(let i = 105; i < 111; i++) {
d(localStorage.getItem("gameState")[i]);
}
d("Enter");
(169 chars)
That saved us a bunch of characters, and I think we've maxed out that kind of refactoring and need to move into gross territory. You see the const
and let
s?
d = (key) => window.dispatchEvent(new KeyboardEvent("keydown", { key }));
for(i = 105; i < 111; i++) {
d(localStorage.getItem("gameState")[i]);
}
d("Enter");
(159 chars)
BAM! Gone! The variables d
and i
are now globals. Obviously you wouldn't do this in actual code, but we just saved a whole 10 characters there!
Where we're declaring i = 105
in the for
loop, we can declare as many variables as we like and separate them with commas. This won't save much, but we can remove a semicolon and a newline by moving our helper function in there too. While we're at it, let's get rid of window.
- dispatchEvent
is in the global scope, so that's just a waste.
for(d = (key) => dispatchEvent(new KeyboardEvent("keydown", { key })), i = 105; i < 111; i++) {
d(localStorage.getItem("gameState")[i]);
}
d("Enter");
(152 chars)
At this point, we've basically done all we can except trimming all that wasteful whitespace:
for(d=(key)=>dispatchEvent(new KeyboardEvent("keydown",{key})),i=105;i<111;i++){d(localStorage.getItem("gameState")[i]);}
d("Enter");
(132 chars)
And finally, because the code block inside the for
loop is a one-liner, we can scrap the curly braces and move the 'Enter' keypress onto the same line, separating the two statements with a semicolon (basically doing this: for(i=0;i<10;i++)console.log(
this works ${i});console.log("now I'm done")
):
for(d=(key)=>dispatchEvent(new KeyboardEvent("keydown",{key})),i=105;i<111;i++)d(localStorage.getItem("gameState")[i]);d("Enter")
And there you have it: 129 characters to automatically beat Wordle. Obviously I had to create a bookmarklet for one-click cheating:
Going to Wordle every day to press the button is pretty annoying, so I guess tomorrow I'm going to have to automate that too.