Wordle solver

Håkan Kjellerstrand (hakank@gmail.com) 
http://hakank.org/

About the first thing in the morning for now 109 days, I solve the  Wordle puzzle (https://www.nytimes.com/games/wordle/index.html). After manually doing this for a few days I realized that it took much time, so I started to figure out the best start word and then also wrote a Wordle solver. 

This solver was - however - done in another programming languages. Here is a port of the solver to Wolfram Language.

Syntax and usage

The constraints

The Wordle solver uses the following approach:
It first figure out what words are allowed based of three filters (constraints):
   * correct position: characters in correct position. These are the Green characters in the Wordle hints.
   * correct chars: characters in the word but in incorrect position. These are the Brown hint characters.
   * chars not in the word. These are the Gray characters.

The call is
     wordle[correctPos,correctChars,notInWord, words]
with the following structure.
     wordle[“.....”,{“”,””,””,””,””},””,words]

Example: 
    wordle[{“i.e..”},{“”,””,””,”n”,”nr”},”pach”,words]
* correct position: We know that the first character is “i” and the third is “e”.
* correct chars: “n” is in the word but not in the the fourth or fifth position. And r in in the word but not in the fifth position
* chars not in word: Neither of characters “p”, “a”, “c”, nor “h” are in the word.

According to the used wordlist, only the word “inert” satisfies these three constraints.

A little more interesting example is the following:
     wordle[“.r.n.”,{“”,””,””,””,””},”slatcoe”,words]
     
Here is a list of the words that match: 
    {bring,brink,briny,drink,drunk,grind,wring,wrung} 
        
This list is in plain alphabetical order, but we don’t get a hint which word is the best to pick. 

Which of these word should we pick?

The scoring (sorting) method

This is where the second part of the system comes into play: the sorting score method. This part might be  of some contribution (I haven’t seen another Wordle solver using exactly this method).

The ideas is to prefer words that are "as common as possible" which is  here translated into sorting the words according to how common the characters are in each position of the words . “Common” here simply means according to frequencies using some wordlist.  The section "Frequency tables for the scoring" below calculates the scores.  

Here's the table of these frequencies . Note that it’s reversed according to the frequency :
     xzyjkquinovhewlrmdgfaptbcs   position 1
     jzqfkgxvbsdymcwptnhulieroa   position 2
     qjhzkxfwyvcbpmgdstlnrueoia   position 3
     qjxyzbwhfvpkmdguotcrilasne   position 4
     jqvuzxbiwfcsgmpoakdnhlrtye   position 5
  
It shows, for example, that for the first position the character "s" is the most common, then "c", then "b", etc ; for the last position of the word “e” is the most common character.  The list is reversed since the score of a word is simply the total of the positions in these strings for the word’s characters . We also throw in a rule that prefers words with unique characters : If the word has 5 distinct characters then it get 100 extra points, otherwise  no extra points .
  
  The words above is sorted according to the scoring method into the following  list :
      {briny, brink, grind, bring, drink, drunk, wring, wrung}
    
The word to pick is thus:  briny

All wordlist words sorted according to the scoring method, and the first guess word

Here are all words in the wordlist sorted according to the score method,  using the empty call 
      wordle[“.....”, {“”, “”, “”, “”, “”}, “”, words]
      
          {saint, crane, crone, coast, brine, crony, cause, briny, boast, slant, poise,
           paint, shine, saner, prone, point, count, corny, clone,
          <<2277>>,
          occur, known, humus, hippo, fuzzy, dizzy, kappa, idyll, pizza,
          offer, ninja, khaki, kebab, rabbi, jiffy, offal, igloo, jazzy, affix}

My own start word (first guess word) is in the first 10 words, but it’ s not “saint” (which is actually quite a good start word). I got my own start word by testing all the words in the wordlist as start words and running the algorithm on the full wordlist: the word that got the best score is now used as start word . Note: These experiments was done using another program a long time ago (about 100 days ago).

Proof of concept: total score so far

I've now used this approach for 109 days and got the following distribution of points :
    1 : 0
    2 : 11
    3 : 38
    4 : 42
    5 : 12
    6 : 5
   missed : 0
 
 This is an average of 3.6422 points. See more on that in the section “Calculate the average of the Wordle scores” below.

The solver

Below is the code for the Wordle solve.  It has a method for each of the three constraints:
 * correct position (checkCorrectPos[])
 * correct chars (checkCorrectChars[]))
 * not in word (checkNotInWord[])
 and for the scoring method mentioned above (sortCandidates[]).

Some of the methods can probably be done neater, especially checkCorrectChars[].  

For sortCandidates[], see the section “Frequency tables for the scoring” above.

(* helper: zipping two lists *)
zip[a_List,b_List] := Transpose[{a,b}];

(* Character in correct position *)
checkCorrectPos[chars_,word_] := Module[{},
StringMatchQ[word,RegularExpression@chars]
]

(* Character in word but in wrong position *)
(* helper *)
checkChars1[word_,chars_] :=StringContainsQ[word,#] &  /@ StringSplit[chars,""];
checkCorrectChars[chars_,word_] := Module[{pos,zs,len=StringLength[word],t1},
zs = zip[StringSplit[word,""],chars];
(* all chars are in word *)
t1 = And @@Flatten[(# == "" || checkChars1[word,#])  & /@ chars,1];
(* no chars is in correct position *)
If[t1,And @@ (#[[2]] == "" || Not[StringContainsQ[#[[1]],#[[2]] ]]& /@ zs)]
]

(* No character in chars are in the word *)
checkNotInWord[chars_,word_] := Module[{},
And @@ (chars == "" || 
Not[StringContainsQ[word, #]] & /@StringSplit[chars,""])
]

(* Sort the candidate words. *)
sortCandidates[candidates_]  := Module[{alphas,scores},
 (* This is the frequency order (reversed) for each positions 
   of the word.   *)
alphas={"xzyjkquinovhewlrmdgfaptbcs",
               "zqfkgxvbsdymcwptnhulieroaj",
               "qjhzkxfwyvcbpmgdstlnrueoia",
               "qjxyzbwhfvpkmdguotcrilasne",
              "qvjuzxbiwfcsgmpoakdnhlrtye"};
SortBy[candidates,scoreWord[#,alphas] &]//Reverse

]

(* Score a word *)
scoreWord[word_,alphas_] := Module[{score1,score2},
(* The score is the total of its positions in the alphas strings above + 
   a 100 points credit if the word has unique characters.  *)
score1 = Total@(StringPosition[alphas[[#]],StringTake[word,{#}]][[1,1]]  & /@  Range[5]);
score2 = If[Length[DeleteDuplicates[StringSplit[word,""]]] == 5,100,0];
score1 + score2
] 

(* Main function: call all the constraints and sort the candidate words according to the score method  *)
wordle[correctPos_, correctChars_,notInWord_,words_] := Module[{candidates},
candidates = Select[words,checkCorrectPos[correctPos,#]&];
candidates = Select[candidates,checkCorrectChars[correctChars,#]&];
candidates = Select[candidates,checkNotInWord[notInWord,#]&];
sortCandidates[candidates]
]

Here are some examples, but let’s first load the word list. Here I’ve used a simple Wordle word list which contains all the 2315 Wordle target words.

wordlist = "wordle_small.txt"
words = StringSplit[Import[wordlist],"\n"];
Length[words]

(* All words *)
AbsoluteTiming[Short[wordle[".....",{"","","","",""},"",words],5]]

wordle["i.e..",{"","","nr","n",""},"sla",words] //AbsoluteTiming

wordle["...n.",{"","","","",""},"slat",words]//AbsoluteTiming

wordle[".r.n.",{"","","","",""},"slatcoe",words]//AbsoluteTiming

wordle[".....",{"s","a","a","s","e"},"lntcu",words]//AbsoluteTiming

Frequency tables for the scoring

The main feature of the scoring method is the (reversed) list of the characters sorted in order the most common characters for each position in the words (from some  wordlist.)

createFreq[words_] := Module[{t,map},
map = Table[Association[],{i,1,5}];
Do[
Do[
t= StringTake[w,{i}];
map[[i]][[t]] = Lookup[map[[i]],t,0]+1,
{i,1,5}],
{w,words}];
Reverse@Flatten[{Keys@Reverse[SortBy[#,2]], Complement[Alphabet[],Keys[ #]]}]  & /@ map
]
(* Shorter version *)
createFreq2[words_] := Module[{k},
Table[k=
    Keys[SortBy[Counts[StringTake[#,{pos}] & /@ words],2]];
StringJoin[Flatten[{Complement[Alphabet[],k],k}],""]
,
{pos,1,5}]
]
(* createFreq[words] *)
createFreq2[words] // TableForm

Calculate the average of the Wordle scores

Here is a simple calculator of the average of one’s Wordle scores.

calcWordleStat[scores_] := Module[{k,v,score}, 
{k,v} = {Keys[scores],Values[scores]};
score=Total[k*v] / Total[v];
{score,score //N}
]

As mentioned above, after 108 rounds I’ve got an average of about 3.65.

scores = <|1->0, 2->11, 3-> 39, 4-> 42, 5-> 12,6-> 5|>;
calcWordleStat[scores]