We have:
- a list of typed letters (and we know what position each word is in, relative to a 5-letter word)
- we know, for each letter, whether it's in the word or not and if it's in the word, whether it's in the right position or not.
- and we have a master-list of words
What remains is a way to compare the clues with the master list to produce a shortlist of candidate words.
isWordACandidate : Array.Array Letter -> String -> Bool
isWordACandidate letters wordToCompare =
if hasExcludedChar letters wordToCompare then
False
else
let
lettersInPosition =
Array.filter (\l -> l.status == InPosition) letters
lettersNotInPosition =
Array.filter (\l -> l.status == NotInPosition) letters
in
hasCharNotInPositionForAllChars (Array.map convertLetterToIndexedChar lettersNotInPosition) wordToCompare && hasCharInPositionForAllChars (Array.map convertLetterToIndexedChar lettersInPosition) wordToCompare
convertLetterToIndexedChar : Letter -> { char : String, index : Int }
convertLetterToIndexedChar { char, index } =
if modBy 5 index == 0 then
{ char = char, index = 5 }
else
{ char = char, index = modBy 5 index }
hasCharInPositionForAllChars : Array.Array { char : String, index : Int } -> String -> Bool
hasCharInPositionForAllChars xs word =
Array.all (\x -> hasCharInPosition x word) xs
hasCharInPosition : { char : String, index : Int } -> String -> Bool
hasCharInPosition { char, index } word =
if String.contains char word then
word
|> String.split ""
|> Array.fromList
|> Array.indexedMap
(\idx c -> c == char && (idx + 1) == index)
|> Array.any ((==) True)
else
False
hasCharNotInPositionForAllChars : Array.Array { char : String, index : Int } -> String -> Bool
hasCharNotInPositionForAllChars xs word =
Array.all (\x -> hasCharNotInPosition x word) xs
hasCharNotInPosition : { char : String, index : Int } -> String -> Bool
hasCharNotInPosition { char, index } word =
if String.contains char word then
word
|> String.split ""
|> Array.fromList
|> Array.indexedMap
(\idx c -> c == char && (idx + 1) == index)
|> Array.any ((==) True)
|> not
else
False
hasExcludedChar : Array.Array Letter -> String -> Bool
hasExcludedChar excludedChars word =
Array.any (\c -> c.status == NotInWord && String.contains c.char word) excludedChars
view : Model -> Html Msg
view model =
div
[ Attr.style "padding" "2rem"
, Attr.style "display" "grid"
, Attr.style "grid-template-columns" "repeat(2, 500px)"
, Attr.style "grid-gap" "2rem"
]
[ div [] [ viewPlayground model ]
, div [] [ viewResults model ]
]
viewResults : Model -> Html Msg
viewResults model =
div [ Attr.style "border" "1px dotted gray", Attr.style "padding" "1rem" ]
[ div [ Attr.style "margin-bottom" "1rem" ] [ text "Candidates:" ]
, div
[ Attr.style "whitespace" "wrap" ]
[ text (String.join ", " (List.filter (isWordACandidate model.typedChars) model.words)) ]
]
viewPlayground : Model -> Html Msg
viewPlayground model =
div
[ Attr.style "display" "grid"
, Attr.style "grid-template-columns" "repeat(5,48px)"
, Attr.style "gap" "10px"
]
<|
List.map viewLetter (Array.toList model.typedChars)
Let's break this code down.
The broad idea is this:
- we have a master list of words.
- we are going to "filter" this list by comparing each word in the list to the "clues".
- that essentially means we apply a
filter
function on the list.
The largest chunk of the code is all about that filter function. It's called isWordACandidate
.
isWordACandidate : Array.Array Letter -> String -> Bool
isWordACandidate letters wordToCompare =
if hasExcludedChar letters wordToCompare then
False
else
let
lettersInPosition =
Array.filter (\l -> l.status == InPosition) letters
lettersNotInPosition =
Array.filter (\l -> l.status == NotInPosition) letters
in
hasCharNotInPositionForAllChars (Array.map convertLetterToIndexedChar lettersNotInPosition) wordToCompare && hasCharInPositionForAllChars (Array.map convertLetterToIndexedChar lettersInPosition) wordToCompare
What's happening is:
- we first check if the word (from the master list) has any character that has been marked as
NotInWord
. If yes, we immediately filter that word out. (This is a bug as you'll see later, but OK for now/most use-cases). - then, we check if the word (from the master list) has letters that have been marked as "NotInPosition" through the
hasCharNotInPositionForAllChars
function (which in itself is a composition of another smaller helper function). This function ensures that the words marked as "NotInPosition" do appear in the word (from the master list) but at the same time, they do not appear in the same position as that in the guessed word. (That is, if 'S' is marked as position 1, all words starting with 'S' are weeded out but words that have 'S' elsewhere in the word will be accepted. And this is repeated for all the other letters marked so.) - then we check if letters marked as
InPosition
do appear in the word (from the master list) and in the same position as in the guessed word.
To construct these functions, we go from the atomic level.
For example, to check and compare a word with a guess for all letters marked as InPosition
, we first have a function that checks it for just one letter:
hasCharInPosition : { char : String, index : Int } -> String -> Bool
hasCharInPosition { char, index } word =
if String.contains char word then
word
|> String.split "" -- first split the word from the master list
-- this produces a (List String)
|> Array.fromList -- convert it into (Array String)
-- because we need to do an `indexedMap`
|> Array.indexedMap -- then map over it with the index
(\idx c -> c == char && (idx + 1) == index) -- and check
-- if the indices are equal and the character is the same
-- as in the guess.
-- this step produces (Array Bool)
|> Array.any ((==) True) -- check if the condition is true for
-- any one letter in the word
else
False
As an example:
let word from master list = PLAYS
let guessed word = PINES
And let's say you marked 'P' as `InPosition` (so, green)
hasCharInPosition ({ char = "P", index = 1 }) "PLAYS" =
0. does P exist in PLAYS? Yes...
1. split word = [ P, L, A, Y, S ]
2. index map and compare =>
1. P -> char == c? (P == P) => true
2. idx + 1 (0 + 1) = index (1) => true
3. so, return True
4. L -> char == c? (L == P) => false
5. so, return False
6. ... and so on for other letters in the word.
3. Array.any ((==) True) [ True, False, False, False, False ] => True!
So for char=P, index=1, "PLAYS" will return True.
If you do the same for char=T, index=1, this whole process will return False.
Then, we use this atomic function to create a larger function that compares the whole list of typedChars.
hasCharInPositionForAllChars : Array.Array { char : String, index : Int } -> String -> Bool
hasCharInPositionForAllChars xs word =
Array.all (\x -> hasCharInPosition x word) xs
We do the same thing for the letters that are in the word but not in position:
hasCharNotInPosition : { char : String, index : Int } -> String -> Bool
hasCharNotInPosition { char, index } word =
if String.contains char word then
word
|> String.split ""
|> Array.fromList
|> Array.indexedMap
(\idx c -> c == char && (idx + 1) == index)
|> Array.any ((==) True)
|> not
else
False
hasCharNotInPositionForAllChars : Array.Array { char : String, index : Int } -> String -> Bool
hasCharNotInPositionForAllChars xs word =
Array.all (\x -> hasCharNotInPosition x word) xs
The core trick in this logic is that:
- we split the
typedChars
based on theirStatus
- we use different comparing functions for different
Status
es (likehasExcludedChar
forNotInWord
,hasCharNotInPositionForAllChars
forNotInPosition
etc.) - and we combine these comparing functions to create the
isWordACandidate
filter function.
Finally, we modify the view function so that the left-side is the playground (where the input letters show up and we can toggle their status) and the right-side is where the program shows the shortlist of words.
view : Model -> Html Msg
view model =
div
[ Attr.style "padding" "2rem"
, Attr.style "display" "grid"
, Attr.style "grid-template-columns" "repeat(2, 500px)"
, Attr.style "grid-gap" "2rem"
]
[ div [] [ viewPlayground model ]
, div [] [ viewResults model ]
]
viewResults : Model -> Html Msg
viewResults model =
div [ Attr.style "border" "1px dotted gray", Attr.style "padding" "1rem" ]
[ div [ Attr.style "margin-bottom" "1rem" ] [ text "Candidates:" ]
, div
[ Attr.style "whitespace" "wrap" ]
[ text (String.join ", " (List.filter (isWordACandidate model.typedChars) model.words)) ]
]
viewPlayground : Model -> Html Msg
viewPlayground model =
div
[ Attr.style "display" "grid"
, Attr.style "grid-template-columns" "repeat(5,48px)"
, Attr.style "gap" "10px"
]
<|
List.map viewLetter (Array.toList model.typedChars)
iewLetter : Letter -> Html Msg
viewLetter word =
let
bgColor =
case word.status of
NotInWord ->
"gainsboro"
NotInPosition ->
"moccasin"
InPosition ->
"yellowgreen"
in
div
[ Attr.style "display" "flex"
, Attr.style "justify-content" "center"
, Attr.style "align-items" "center"
, Attr.style "width" "44px"
, Attr.style "height" "44px"
, Attr.style "border" ("1px solid " ++ bgColor)
, Attr.style "background" bgColor
, Attr.style "text-transform" "uppercase"
, Attr.style "cursor" "default"
, Events.onClick (Toggle word.index)
]
[ text word.char ]
This produces:
The full source-code can be found here.
Some closing thoughts, bugs to fix, ideas to explore further etc.
- I do a
mapError
in thegetWords
API call. This effectively throws away the error and returns just a string called "Error" if there was an error in fetching. This could be improved. - Also notice how pointless it is to do a
Debug.log
in the error branch ofGotWords
. (Because it's always going to print "Error" because of themapError
). - On the UI, we don't show anything if we do hit a snag fetching the master words list. Nor do we show a loading state while the fetch happens.
- More critical: you can actually type numbers! You shouldn't be able to. Explore how to resolve this bug by making use of
charCode
. - Even more critical: there is a bug in the logic. If you typed a word with repeating letters (like GUESS), and Wordle says one S is yellow (in word, wrong position) and other S is grey (not in word), our logic will fail and show no results.
- The word list we use often falls short. (eg "Polyp" does not figure on the list!)
Top comments (0)