DeWordle

Published Sunday, January 23, 2022 by Bryan

Like the rest of the English-tweeting world, I've been playing Wordle. I've been lucky that no one in my twitter network is posting spoilers. They're only posting the colored-squares diagram overviews of their games.

Wait. Are those spoilers?

I had wondered this idly before, but then a couple of days ago, one of the diagrams went like so:

🟩🟩⬜⬜🟩
🟩🟩⬜⬜🟩
🟩🟩⬜⬜🟩

Having also played Absurdle a few times, I remembered its discussion of unfortunate guess sequences. The example given was WIPER, RIVER, LINER, CIDER, looking like:

⬜🟩⬜🟩🟩
⬜🟩⬜🟩🟩
⬜🟩⬜🟩🟩
⬜🟩⬜🟩🟩

And so, I got to thinking about what words might have a 🟩🟩⬜⬜🟩 shape. What came to mind was PUFFY, BUDDY, MUDDY. When I found the solution, the double letter turned out to be wrong, but the Y was right.

Was it luck, or am I onto something? If I looked at nothing other than colored-tile diagrams, could I surmise the day's word? Let's find out.

It's tempting to try to reason about how one guess might have led to another. It's tempting to guess that one less yellow and one more green on the next line probably means that it's the same letter, put in the correct place. But guessing in this way isn't required, and many people purposefully guess completely different letters on subsequent lines, to rule out more letters faster.

The only deduction we can make for certain is the one imposed by the rules: that there exists a word with matching or near-matching letters in this pattern. That doesn't seem like much. There are 2,315 possible answer words, and an additional 10,657 words you're allowed to guess, but that will never be answers. And there are 243 possible square patterns: three different colors of square in each of five positions.

To work backward from a list of guess tiles to the letters that might have been used in that guess, we need to think about this question: given two guess patterns, what answer words have answer and non-answer words that when guessed would give these patterns? If the answer is that every answer word has guess words that produce every pattern, then this scheme isn't going to work at all. But, if, for each answer word, there are one or more patterns that no guess word will ever produce, then we can whittle down the possible set of answers.

So let's find out which answers use which patterns. Two hundred forty-three patterns and 2,315 words isn't many, so I'm just going to use a map datastructure. My keys are the single guess diagrams (i.e. one row of tiles), and the value attached to each key is the list of answer words that have guess words that would lead to those diagrams.

-module(dewordle).

generate_map(Possible, Impossible) ->
    All = Possible ++ Impossible,
    Map = maps:from_keys(lists:seq(0,242), []),
    WordMap = lists:foldl(fun(Word, Acc) ->  map_word(All, Word, Acc) end,
                          Map,
                          Possible),
    maps:map(fun(_K, V) -> ordsets:from_list(V) end, WordMap).

map_word(All, Word, Map) ->
    Scores = lists:foldl(fun(Guess, Acc) ->
                                 ordsets:add_element(
                                   score_guess(Guess, Word), Acc)
                         end,
                         [],
                         All),
    lists:foldl(fun(Score, Acc) ->
                        #{Score := SoFar} = Acc,
                        Acc#{Score := [Word|SoFar]}
                end,
                Map,
                Scores).

score_guess(Guess, Word) ->
    {UsedGuess, UsedWord} = score_correct(Guess, Word),
    list_to_integer(score_rest(UsedGuess, UsedWord), 3).

score_correct(Guess, Word) ->
    {RevG, RevW} = lists:foldl(fun({M, M}, {GS, WS}) ->
                                       %% purposefully non-matching for later
                                       {[$2|GS], [2|WS]};
                                  ({G, W}, {GS, WS}) ->
                                       {[G|GS], [W|WS]}
                               end,
                               {[], []},
                               lists:zip(Guess,Word)),
    {lists:reverse(RevG), lists:reverse(RevW)}.

score_rest(Guess, Word) ->
    {RevScore, _} = lists:foldl(fun($2, {Score, Acc}) ->
                                        {[$2|Score], Acc};
                                   (G, {Score, Acc}) ->
                                        case lists:splitwith(
                                               fun(L) -> L =/= G end,
                                               Acc) of
                                            {Head, [G|Tail]} ->
                                                {[$1|Score], Head++[1|Tail]};
                                            _ ->
                                                {[$0|Score], Acc}
                                        end
                                end,
                                {[], Word},
                                Guess),
    lists:reverse(RevScore).

Instead of using the emoji symbols of the tile diagrams as keys, I used the integers 0 through 242. I converted the diagrams to integers by replacing green squares (correct letter in correct position) with the number '2', yellow squares (correct letter, wrong position) with '1', and white squares (incorrect letter) with '0', then interpretted the string in base 3. That is ⬜⬜⬜⬜⬜ = "00000" = 0, and 🟩🟩🟩🟩🟩 = "22222" = 242, and 🟩⬜⬜⬜🟨 = "20001" = 163.

What did we come up with? As a quick smoke check, all 2,315 words should appear in the 242 bin, because that's how you win:

Map = dewordle:generate_map().
2315 = length(maps:get(242, Map)).

Hooray! Now … how many other patterns do all answer words have guesses for?

AllWords = maps:keys(maps:filter(fun(_, V) -> length(V) == 2315 end, Map)).
% [0,1,2,3,9,10,12,18,27,81,84,90,108,242]

Zero is an interesting inclusion. It doesn't help our deduction much, but for every answer word, there is at least one guess word that shares no letters in common. Here's that whole list visually:

format_score(Score) ->
    Number = integer_to_list(Score, 3),
    io:format("~ts~n",
              [[ case P of
                     $2 -> 16#1f7e9;
                     $1 -> 16#1f7e8;
                     $0 -> 16#2b1c
                 end
                 || P <-  lists:duplicate(5-length(Number), $0) ++ Number]]).
[ dewordle:format_score(S) || S <- AllWords ].

This prints:
⬜⬜⬜⬜⬜
⬜⬜⬜⬜🟨
⬜⬜⬜⬜🟩
⬜⬜⬜🟨⬜
⬜⬜🟨⬜⬜
⬜⬜🟨⬜🟨
⬜⬜🟨🟨⬜
⬜⬜🟩⬜⬜
⬜🟨⬜⬜⬜
🟨⬜⬜⬜⬜
🟨⬜⬜🟨⬜
🟨⬜🟨⬜⬜
🟨🟨⬜⬜⬜
🟩🟩🟩🟩🟩

So every answer also has at least one guess that has exactly one correct letter in the wrong position (1, 3, 9, 27, 81), and every answer also has at least one guess that has the correct letter in the right position at the end (2) and in the middle (18). With five additional right-letter-wrong-place patterns being common, we've lost 14 patterns in our whittling hopes.

What about patterns we'll never see?

NoWords = maps:keys(maps:filter(fun(_, V) -> V == [] end, Map)).
% [161,215,233,239,241]

Visually:
🟨🟩🟩🟩🟩
🟩🟨🟩🟩🟩
🟩🟩🟨🟩🟩
🟩🟩🟩🟨🟩
🟩🟩🟩🟩🟨

Only five patterns, and none of them are surprising: you can't have four letters in their correct positions, and have the fifth letter be correct but in the wrong position. So we're looking at 224 (= 243 - 14 - 5) unique patterns that will give us some information. What pattern is used by the fewest words?

lists:keysort(2, maps:to_list(
                     maps:filtermap(fun(_, []) -> false;
                                       (_, V) -> {true, length(V)}
                                    end,
                                    Map))).
% [{149,15},
%  {131,22},
%  {134,30},
%  {211,32},
% ...

Wow. If we ever see 🟨🟩🟨🟨🟩 (= 149 = 12112 in base 3), we already know that there are only 15 possible answers. If we ever see 🟩🟨🟩🟨🟨 (= 211 = 21211 in base 3), we know that there are only 32 possible answers.

words_for_scores(Scores, Map) ->
    #{242 := AllWords} = Map,
    lists:foldl(fun(Score, Acc) ->
                        #{Score := Words} = Map,
                        ordsets:intersection(Words, Acc)
                end,
                AllWords,
                Scores).

dewordle:words_for_scores([149, 211], Map).
% ["ALIEN"]

But here is what we were hoping for: if we ever see a day where people observe both 🟨🟩🟨🟨🟩 and also 🟩🟨🟩🟨🟨, then we know the answer exactly. "ALIEN" is the only answer with guess words that will generate both of those patterns. Now the big question: how many words can be uniquely identified by their guess patterns?

words_with_same_score_as(Word, Map) ->
    #{242 := AllWords} = Map,
    maps:fold(fun(_, V, Acc) ->
		      case lists:member(Word, V) of
			  true ->
			      ordsets:intersection(V, Acc);
			  false -> Acc
		      end
	      end,
	      AllWords,
	      Map).

classify_words(Map) ->
    #{242 := AllWords} = Map,
    lists:foldl(fun(Word, {Single, Multiple}) ->
			case words_with_same_score_as(Word, Map) of
			    [Word] ->
				{[Word|Single], Multiple};
			    Many ->
				{Single, [{Word,Many}|Multiple]}
			end
		end,
		{[], []},
		AllWords).

{Single, Multiple} = dewordle:classify_words(Map).
2151 = length(Single).
164 = length(Multiple).

All but 164 words have completely unique score signatures! That's already better than I was hoping. Are the other 164 doomed to guessing?

[] = lists:filter(fun({W, Other}) ->
                      lists:any(fun(Q) ->
                                    (Q =/= W) and
                                        (not lists:member(Q, Single))
                                end,
                                Other)
                  end,
                  Multiple).

They're not! Every "overlapping" word for each of our 164 words with non-unique score signatures has its own unique signature.

So at least in theory, every Wordle answer word can be uniquely identified by the guess patterns in its game. Does this actually work on real-world data? Will enough people encounter enough patterns, and tweet them, that I can figure out the word without knowing any of the letters guessed? Let's find out.

It's still January 23, aka Wordle 218, where I live. Can I predict what Wordle 219 will be? Let's search twitter for "Wordle 219".

@omeroz's Wordle 219 diagram
2094 = length(dewordle:words_for_scores([3#00210, 3#00201, 3#22200], Map)).

The score syntax is "3#" to tell the shell to interpret the rest of the number in base 3, then the 0, 1, 2 pattern I've described above. Copying and pasting strings of emoji colored squares between my terminal, Safari, and emacs doesn't go as easily as I'd like. I omitted both the all-white and the all-green lines, since we know they don't give us any information. The three "interesting" patterns only reduce our answer space by 221 words (=2315-2094). Let's get a couple more.

@beckytopia's Wordle 219 diagram
2037 = length(dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200, % @omermeroz
                  3#01020, 3#10002           % @beckytopia
              ], Map)).
@sheyaghosal's Wordle 219 diagram
1755 = length(dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200, % @omermeroz
                  3#01020, 3#10002,          % @beckytopia
                  3#01210, 3#02200           % @shreyaghoshal
              ], Map)).
@doriancraycray's Wordle 219 diagram
1474 = length(dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200, % @omermeroz
                  3#01020, 3#10002,          % @beckytopia
                  3#01210, 3#02200,          % @shreyaghoshal
                  3#00100, 3#01201, 3#10210  % @doriancraycray
              ], Map)).
@JackyNinjakitty's Wordle 219 diagram
1317 = length(dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200, % @omermeroz
                  3#01020, 3#10002,          % @beckytopia
                  3#01210, 3#02200,          % @shreyaghoshal
                  3#00100, 3#01201, 3#10210, % @doriancraycray
                  3#00010, 3#01120           % @JakyNinjakitty
              ], Map)).
@anniedundun's Wordle 219 diagram
678 = length(dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211                            % @anniedundun
              ], Map)).

It took a few rounds, but we're down to only 678 possible answers. Over 70% of the answer space eliminated! That last tweet was key. I've tried this process on a few days that I knew the answer to as well. Scores that include lots of yellow (correct letter, incorrect position) with at least one green seem to have the most influence in reducing the search space. Experience on other days also tells me that narrowing from here is going to be slower if we don't accidentally find another "good" score. What score(s) would make the most difference from here?

Scores678 = [
             3#00210, 3#00201, 3#22200,         % @omermeroz
             3#01020, 3#10002,                  % @beckytopia
             3#01210, 3#02200,                  % @shreyaghoshal
             3#00100, 3#01201, 3#10210,         % @doriancraycray
             3#00010, 3#01120,                  % @JakyNinjakitty
             3#01211                            % @anniedundun
            ].
Better678 = lists:foldl(
    fun(Score, Acc) ->
        case length(dewordle:words_for_scores([Score|Scores678], Map)) of
            N when N > 0, N < 678 ->
                [{Score, N}|Acc];
            _ ->
                Acc
        end
    end,
    [],
    lists:seq(0,242)).
lists:keysort(2, Better678).
% [{149,8},
%  {134,9},
%  {131,11},
%  {151,14},
%  {160,15},

Cool. So any of 🟨🟩🟨🟨🟩 (=149), 🟨🟨🟩🟩🟩 (=134), 🟨🟨🟩🟨🟩 (=131), etc. would reduce our list to something very short. Anyone see those? I haven't checked them all, but so far the answer is "no". The unfortunate part of this method is that we can't safely discard patterns that no one has seen. It might just be that noone has seen it yet. So we have to keep looking for patterns that people have seen. Before I do too much more of what a former manager used to call "fishing with rocks", let's see if we can get any closer.

Thank you, @SoWhoIsAmber, @MR0808, @fitztiptoes, @vibinjabakar, @nonaness_, @Turbidarrow212, @jackomarto, @kuppanoodle, @JordanRasko, @snackynicky, and @zalmaaaa.

115 = length(dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121                            % @zalmaaaa
              ], Map)).
Scores115 = [
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121                            % @zalmaaaa
            ].
Better115 = lists:foldl(
    fun(Score, Acc) ->
        case length(dewordle:words_for_scores([Score|Scores115], Map)) of
            N when N > 0, N < 115 ->
                [{Score, N}|Acc];
            _ ->
                Acc
        end
    end,
    [],
    lists:seq(0,242)).
Even115 = lists:foldl(
    fun(Score, Acc) ->
        case length(dewordle:words_for_scores([Score|Scores115], Map)) of
            N when N == 115 ->
                [{Score, N}|Acc];
            _ ->
                Acc
        end
    end,
    [],
    lists:seq(0,242)).
161 = length(Better115).
77 = length(Event115).
lists:keysort(2, Better115).
% [{134,1},
%  {160,2},
%  {149,2},
%  {131,2},
%  {238,3},
lists:reverse(lists:keysort(2, Better115)).
% [{57,114},
%  {63,114},
%  {198,114},
%  {22,113},
%  {110,113},

Now we're really getting somewhere. Let's try fishing with rocks again. Thanks, @PulloutMethyd and @taomeslibrary.

114 = length(dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121,                           % @zalmaaaa
                  3#02010                            % @PulloutMethyd
              ], Map)).
Scores114 = [
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121,                           % @zalmaaaa
                  3#02010                            % @PulloutMethyd
              ].
Better114 = lists:foldl(
    fun(Score, Acc) ->
        case length(dewordle:words_for_scores([Score|Scores114], Map)) of
            N when N > 0, N < 114 ->
                [{Score, N}|Acc];
            _ ->
                Acc
        end
    end,
    [],
    lists:seq(0,242)).
160 = length(Better114).
lists:reverse(lists:keysort(2, Better114)).
% [{63,113},
%  {198,113},
%  {22,112},
%  {110,112},
112 = length(dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121,                           % @zalmaaaa
                  3#02010,                           % @PulloutMethyd
                  3#00211                            % @taomeslibrary
              ], Map)).
Scores112 = [
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121,                           % @zalmaaaa
                  3#02010,                           % @PulloutMethyd
                  3#00211                            % @taomeslibrary
              ].
Better112 = lists:foldl(
    fun(Score, Acc) ->
        case length(dewordle:words_for_scores([Score|Scores112], Map)) of
            N when N > 0, N < 112 ->
                [{Score, N}|Acc];
            _ ->
                Acc
        end
    end,
    [],
    lists:seq(0,242)).
159 = length(Better112).
lists:reverse(lists:keysort(2, Better112)).
[{63,111},
 {198,111},
 {110,110},
 {164,110},

This is very slow whittling. But the thing is, all of the scores that immediately jump to one or two words from here are guesses that have zero incorrect letters. No one seems to be guessing that for puzzle 219. I'm having a little bit of success at the other end, where there are still guesses that have a couple of incorrect letters. But even there, I have two patterns that aren't showing up. Let's try just a few more rounds. Thanks, @wildestays and @yasharmouta.

65 = length(dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121,                           % @zalmaaaa
                  3#02010,                           % @PulloutMethyd
                  3#00211,                           % @taomeslibrary
                  3#01200, 3#11002,                  % @wildestays
                  3#00022, 3#22022                   % @yasharmouta
              ], Map)).
Scores65 = [
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121,                           % @zalmaaaa
                  3#02010,                           % @PulloutMethyd
                  3#00211,                           % @taomeslibrary
                  3#01200, 3#11002,                  % @wildestays
                  3#00022, 3#22022                   % @yasharmouta
              ].
Better65 = lists:foldl(
    fun(Score, Acc) ->
        case length(dewordle:words_for_scores([Score|Scores65], Map)) of
            N when N > 0, N < 65 ->
                [{Score, N}|Acc];
            _ ->
                Acc
        end
    end,
    [],
    lists:seq(0,242)).
156 = length(Better65).
lists:reverse(lists:keysort(2, Better65)).
% [{14,64},
%  {63,64},
%  {183,64},
%  {198,64},
%  {40,63},
%  {64,63},

Aha, a stroke of luck! My twitter search for ⬜🟩🟩⬜🟨 matched @yasharmouta's tweet, because it wrapped around the line ending between ⬜⬜⬜🟩🟩 and ⬜🟨⬜🟩🟩. While it wasn't what we were looking for, 🟩🟩⬜🟩🟩 is in our search list, and halves the answer space. Thanks, @kdiizzles, uwuttaker, @countjazula, @fisforfavorites, @fel_clt, and @spikeymikeyYT.

10 = length(dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121,                           % @zalmaaaa
                  3#02010,                           % @PulloutMethyd
                  3#00211,                           % @taomeslibrary
                  3#01200, 3#11002,                  % @wildestays
                  3#00022, 3#22022,                  % @yasharmouta
                  3#10010, 3#11110,                  % @kdiizzles
                  3#22010,                           % @uwuttaker
                  3#02020,                           % @countjazula
                  3#00002, 3#01002, 3#00101, 3#02022,% @fisforfavorites
                  3#12000, 3#20102, 3#20012,         % @fel_clt
                  3#21021, 3#21011                   % @spikeymikeyYT
              ], Map)).
Scores10 = [
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121,                           % @zalmaaaa
                  3#02010,                           % @PulloutMethyd
                  3#00211,                           % @taomeslibrary
                  3#01200, 3#11002,                  % @wildestays
                  3#00022, 3#22022,                  % @yasharmouta
                  3#10010, 3#11110,                  % @kdiizzles
                  3#22010,                           % @uwuttaker
                  3#02020,                           % @countjazula
                  3#00002, 3#01002, 3#00101, 3#02022,% @fisforfavorites
                  3#12000, 3#20102, 3#20012,         % @fel_clt
                  3#21021, 3#21011                   % @spikeymikeyYT
              ].
Better10 = lists:foldl(
    fun(Score, Acc) ->
        case length(dewordle:words_for_scores([Score|Scores10], Map)) of
            N when N > 0, N < 10 ->
                [{Score, N}|Acc];
            _ ->
                Acc
        end
    end,
    [],
    lists:seq(0,242)).
113 = length(Better10).
lists:reverse(lists:keysort(2, Better10)).
% [{23,9},
%  {25,9},
%  {34,9},
%  {66,9},
%  {78,9},
lists:keysort(2, Better10).               
% [{238,1},
%  {231,1},
%  {230,1},
%  {223,1},

Okay, now we have something even more interesting. There are only ten words left. But, there are still 113 patterns that, when added to our existing list, reduce the size of the answer space, without eliminating all possibilities. It feels like there should be a better way to go through these, but there's just no way to tell which pattern someone might have tried. We can't even say, for sure, that if none of the patterns for one of the ten words was found, then that word is definitely not the answer. I'm going to give the optimistic end of the list one more try.

@echipir_'s Wordle 219 diagram
["SPIEL"] = dewordle:words_for_scores([
                  3#00210, 3#00201, 3#22200,         % @omermeroz
                  3#01020, 3#10002,                  % @beckytopia
                  3#01210, 3#02200,                  % @shreyaghoshal
                  3#00100, 3#01201, 3#10210,         % @doriancraycray
                  3#00010, 3#01120,                  % @JakyNinjakitty
                  3#01211,                           % @anniedundun
                  3#01022,                           % @SoWhoIsAmber
                  3#01001, 3#01110, 3#21210,         % @MR0808
                  3#00200, 3#00222,                  % @fitztiptoes
                  3#01010, 3#20110, 3#21000,         % @vibinjabakar
                  3#22000,                           % @nonaness_
                  3#22002,                           % @Turbidarrow212
                  3#02202,                           % @jackomarto
                  3#11020,                           % @kuppanoodle
                  3#00220,                           % @JordanRasko
                  3#10012, 3#10110,                  % @snackynicky
                  3#00121,                           % @zalmaaaa
                  3#02010,                           % @PulloutMethyd
                  3#00211,                           % @taomeslibrary
                  3#01200, 3#11002,                  % @wildestays
                  3#00022, 3#22022,                  % @yasharmouta
                  3#10010, 3#11110,                  % @kdiizzles
                  3#22010,                           % @uwuttaker
                  3#02020,                           % @countjazula
                  3#00002, 3#01002, 3#00101, 3#02022,% @fisforfavorites
                  3#12000, 3#20102, 3#20012,         % @fel_clt
                  3#21021, 3#21011,                  % @spikeymikeyYT
                  3#22112                            % @echipir_
              ], Map).

We have an answer! I tried very hard during my searching to not accidently read a spoiler, and I am so surprised by this answer that I think I succeeded. Suddenly the complaints of difficulty and, "Off to the dictionary to find out what this means," comments make sense.

I'm amazed that this seems to have worked. Yes, "seems to have", because there is a chance that my answer is wrong. This method is the epitome of "garbage in, garbage out". If I somehow picked up a false Wordle diagram, or mistyped my translation of the diagram into my shell, I might have arrived at the wrong word. I need to wait another 3 hours to find out for sure.

Assuming my answer is correct, though, is this an effective way to "cheat" at Wordle? Not in the way I did it, no. It has taken me more than an hour - maybe two or three, I zoned out and lost track - to find enough samples. It also depends on lots of people tweeting. It became obvious during my search that there were a few common patterns people saw with their guesses. Since I don't know the actual letters anyone guessed (thought I suppose I could work backward to a few probabilities now), I don't know if that's because people use similar starting words, or some other reason. In any case ` ⬜🟨🟩⬜🟨 was a common sight in both short and long diagrams.

If anyone is looking for a fun twitter-firehose-consuming project, it could be a lot of fun to set up a bot to watch for new Wordle tweets, aggregate samples automatically, and announce when it has enough to answer. The filtering of "joke" answers, answers from Wordle clones in other languages, and the like will be a concern. If my code can help in anyway, in addition to being inline above, it's also on Github at beerriot/dewordle.

Anyway, come back in a couple hours to see if SPIEL is, in fact, the correct answer! Update: SPIEL is incorrect. Update 2: Continue on to my next post to find out what went wrong.

Categories: Development