Exploring Wordle with Julia: Part I

Wordle is a fun and easy game that has shot to fame recently. It is completely minimalistic, no-fills and highly addictive. We will explore here how to write as simple solver for Wordle with Julia and then adding layers of complexity and sophistication as we go along.

In Wordle we are successively guessing five-letter words. After each guess the game tells us which of our letters occur in the secret word and whether they are in the correct position. As explained in their website.

So after each guess we narrow the set of possible words to choose from till we arrive at the secret word, and we only have six tries.

Our Dictionary

Wordle only deals with a valid five-letter English words. So we need to start grabbing such words. A good place to start is the Standford GraphBase data set.

julia> words = readlines(download("https://www-cs-faculty.stanford.edu/~knuth/sgb-words.txt"))
5757element Vector{String}:
"which"
"there"
"biffy"
"pupal"
view raw word_dict.jl hosted with ❤ by GitHub

Constraints

The next step is the try narrow down the set of feasible words as we are getting clues. Towards that end, we construct a Dict to map letters that are in the word at the correct spot to their location, another Dict to maps letter in the word that are in the wrong spot, and finally a Set of letters that are not in the word:

let_in_pos = Dict{Int,Char}()
let_not_in_pos = Dict{Char,Vector{Int}}()
let_not_in_word = Set{Char}()

Now we can encode the the response we get the game as String of numbers, where 0 means that the character is not in the word, 1 means the that character is the word but not in the right spot, and 2 means that the character is in the word at the correct spot. For example

Secret Word:    perky
Candidate Word: ebony
Response:       10002

We can update our set of constraint for any response by the following function:

function update_constraints_by_response!(word, response, let_in_pos, let_not_in_pos, let_not_in_word)
for i in eachindex(response)
c = response[i]
if c=='2'
let_in_pos[i]=word[i]
elseif c=='1'
let_not_in_pos[word[i]] = push!(get(let_not_in_pos,word[i],Int[]),i)
else
push!(let_not_in_word,word[i])
end
end
end

Shrinking the feasible set of words

The constraints will reduce the feasible set of possible words. This can be done by applying filters that make use of constraints as in the following function:

function word_set_reduction!(word_set, let_in_pos, let_not_in_pos, let_not_in_word)
filter!(w->all(w[r[1]]==r[2] for r in let_in_pos), word_set)
filter!(w->all(occursin(s[1],w) && all(w[p]!=s[1] for p in s[2]) for s in let_not_in_pos), word_set)
filter!(w->all(!occursin(c,w[setdiff(1:5,keys(let_in_pos))]) for c in setdiff(let_not_in_word,keys(let_not_in_pos))), word_set)
end

Putting it all together

So all what we have to do now is to apply the following step

  1. Guess a candidate first word
  2. Get response from the Game
  3. Use that response to update the set of constraints
  4. Use the constraints to reduce the set of possible words
  5. Guess the new candidate word
  6. Repeat step 2 until a word is found or the game is over

In the steps 1 and 5 above, the simplest approach is just to pick a word at random. Below is Julia repl session showing how we go about applying the above. The secret word here is “super”

julia> let_in_pos = Dict{Int,Char}();
julia> let_not_in_pos = Dict{Char,Vector{Int}}();
julia> let_not_in_word = Set{Char}();
julia> word_set = copy(words);
julia> word=rand(word_set)
"excel"
julia> update_constraints_by_response!(word, "00020", let_in_pos, let_not_in_pos, let_not_in_word)
julia> word_set_reduction!(word_set, let_in_pos, let_not_in_pos, let_not_in_word)
698element Vector{String}:
"other"
"water"
"assed"
"osier"
julia> word=rand(word_set)
"buyer"
julia> update_constraints_by_response!(word, "02022", let_in_pos, let_not_in_pos, let_not_in_word)
julia> word_set_reduction!(word_set, let_in_pos, let_not_in_pos, let_not_in_word)
13element Vector{String}:
"outer"
"super"
"fumer"
"muter"
julia> word=rand(word_set)
"fumer"
julia> update_constraints_by_response!(word, "02022", let_in_pos, let_not_in_pos, let_not_in_word)
julia> word_set_reduction!(word_set, let_in_pos, let_not_in_pos, let_not_in_word)
10element Vector{String}:
"outer"
"super"
"duper"
"nuder"
julia> word=rand(word_set)
"purer"
julia> update_constraints_by_response!(word, "12022", let_in_pos, let_not_in_pos, let_not_in_word)
julia> word_set_reduction!(word_set, let_in_pos, let_not_in_pos, let_not_in_word)
2element Vector{String}:
"super"
"duper"
julia> word=rand(word_set)
"super"

We see that we arrived at the correct word after guessing

  • excel
  • buyer
  • fumer
  • purer
  • super

That is a 5/6 score. Not too bad! But we could do better. How? We explore that in the next part.

See also below an interactive session where the secret word is “wrung”

One thought on “Exploring Wordle with Julia: Part I

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.