Part 2 – Build a Wordle Helper Using Elm and A Little Bit of Logic
Tags: elm, wordle, game, functional
Published: 2023-08-07 14:30:01

---
title: "Part 2 – Build a Wordle Helper Using Elm and A Little Bit of Logic"
published: true
description: "An Elm program to help solve Wordle puzzles."
tags: elm, wordle, game, functional
series: "Build a Wordle Helper Using Elm and A Little Bit of Logic"
---
On Wordle, you can enter 5-letter words. After you enter each word, the app gives you clues by way of color-codes:

- a grey boxed letter means that letter does not exist in the answer word.
- an amber/yellow boxed letter means that letter exists in the word but not in *that* position.
- a green boxed letter means that letter exists in the word and in the same position as you typed.

To help our program shortlist the potential answers from a giant master list, we need to be able to *tell these clues to it*.

We'll keep it simple:

- every keystroke of a valid letter will type a letter on the screen
- we can "click" on the letter-tile / box to mark the letter as
	- not being in the word (grey)
	- being in the word but in a different position (yellow)
	- in the word and in the same position (green)
- backspace will clear each letter (in reverse)

In code:

```elm
-- update imports
-- the previous imports as they are, plus these new ones:
import Array
import Browser.Events as BEvents
import Html exposing (Html, button, div, text)
import Html.Attributes as Attr
import Html.Events as Events
import Json.Decode as JD

-- update the Model to include `typedChars`, a key that contains all typed letters
type alias Model =
    { words : List String, typedChars : Array.Array Letter }

type alias Letter =
    { index : Int, char : String, status : Status }

type Status
    = NotInWord
    | NotInPosition
    | InPosition
    
-- update Msg
type Msg
    = GotWords (Result String String)
    | KeyPress Key
    | Toggle Int


type Key
    = Valid Char
    | Backspace
    | IgnoreKey

-- update init coz model has changed a bit:
init : () -> ( Model, Cmd Msg )
init _ =
    ( { words = [], typedChars = Array.empty }, getWords )

-- update function now has to handle two new `Msg` types:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Toggle index ->
            ( { model
                | typedChars =
                    Array.indexedMap
                        (\idx ltr ->
                            if idx == index - 1 then
                                { ltr | status = toggleStatus ltr.status }

                            else
                                ltr
                        )
                        model.typedChars
              }
            , Cmd.none
            )

        KeyPress key ->
            case key of
                Valid char ->
                    ( { model | typedChars = Array.push (Letter (Array.length model.typedChars + 1) (String.toLower <| String.fromChar char) NotInWord) model.typedChars }, Cmd.none )

                Backspace ->
                    ( { model
                        | typedChars = Array.slice 0 -1 model.typedChars
                      }
                    , Cmd.none
                    )

                _ ->
                    ( model, Cmd.none )

        GotWords res ->
            case res of
                Err e ->
                    let
                        _ =
                            Debug.log "error" e
                    in
                    ( model, Cmd.none )

                Ok wrds ->
                    ( { model
                        | words = String.lines wrds
                      }
                    , Cmd.none
                    )

toggleStatus : Status -> Status
toggleStatus status =
    case status of
        NotInWord ->
            NotInPosition

        NotInPosition ->
            InPosition

        InPosition ->
            NotInWord

-- updating the view function to render the typed letters (along with colored boxes)
view : Model -> Html Msg
view 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)

viewLetter : Letter -> Html Msg
viewLetter letter =
    let
        bgColor =
            case letter.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 letter.index)
        ]
        [ text letter.char ]

-- update the subscriptions so that the program "listens" to all keypress events and responds appropriately:
subscriptions : Model -> Sub Msg
subscriptions _ =
    BEvents.onKeyUp (JD.map KeyPress keyDecoder)

keyDecoder : JD.Decoder Key
keyDecoder =
    let
        toKey val =
            if val == "Backspace" then
                Backspace

            else if val == " " then
                IgnoreKey

            else
                case String.uncons val of
                    Just ( char, "" ) ->
                        Valid char

                    _ ->
                        IgnoreKey
    in
    JD.map toKey (JD.field "key" JD.string)
```


That's some humongous piece of code so let's break it down by the logic.

**First off, we need to "listen" to keypress events so that if we type "S", we show that on screen and also be able to mark/toggle the status of that letter.**

To do this, we can use `Browser.Events`'s `onKeyUp` function.

We need to be able to "process" the event, for which we need to use a `Decoder`. 

```elm
import Json.Decode as JD

type Msg
    = GotWords (Result String String)
    | KeyPress Key --- this is the thing we add

-- We only need to "respond" to two kinds of keystrokes:
-- alphabets AND backspace. Everything else, we ignore.
type Key
    = Valid Char
    | Backspace
    | IgnoreKey

subscriptions : Model -> Sub Msg
subscriptions _ =
    BEvents.onKeyUp (JD.map KeyPress keyDecoder)
```

The `keyDecoder` should help us "decode" the keyup event. 

We only need to "respond" to two kinds of keystrokes: alphabets AND backspace. Everything else, we ignore.

```elm
type Key
    = Valid Char
    | Backspace
    | IgnoreKey

keyDecoder : JD.Decoder Key
keyDecoder =
    let
        toKey val =
            if val == "Backspace" then
                Backspace

            else if val == " " then
                IgnoreKey

            else
                case String.uncons val of
                    Just ( char, "" ) ->
                        Valid char

                    _ ->
                        IgnoreKey
    in
    JD.map toKey (JD.field "key" JD.string)
```

In `JD.field "key" JD.string`, we are extracting the "key" field from the keyup object that the browser throws at us.

The "key" field's value is either the typed letter (like "a", "B", "z", " " [space] etc.) or the name of the control key that was pressed like "Backspace", "Control", "Meta" etc.

We "map" that. That is, we pass it through a transformation function so that the key is transformed into a `Key`, a type which lets us decide what kind of a key was pressed.

```elm
toKey val =
		if val == "Backspace" then
				Backspace
	
		else if val == " " then
				IgnoreKey
	
		else
				case String.uncons val of
						Just ( char, "" ) ->
								Valid char
	
						_ ->
								IgnoreKey
```

In this `toKey` function, we read the value of the key and then change it to `Valid x` or `Backspace` or `IgnoreKey` (that represents a keypress of any key that we want to just ignore).

**Let's talk a bit about our model for storing typed characters.**

```elm
type alias Model =
    { words : List String, typedChars : Array.Array Letter }

type alias Letter =
    { index : Int, char : String, status : Status }

type Status
    = NotInWord
    | NotInPosition
    | InPosition
```

We just store all typed keys (ie, `Valid` ones) in a simple, one-dimensional array. 

You'd think, *how can we then decide which char belongs to which word guess?*

Our `Letter` type also includes an `index` which will start with 1 and increment up.

Since all words are 5-letters long, we can use simple `modBy` math to figure out the position of each letter in a 5-letter word/grid.

Take an array of characters S H I R T G O A T S for example. Let it be something that you typed into the program:

```
S H I R T G O A T S

by `index`, this is:
index    1  2  3  4  5  6  7  8  9  10
letter   S  H  I  R  T  G  O  A  T  S

we can use `modBy 5` on index to figure out the relative position 
of each letter in a 5-letter grid:
index    1  2  3  4  5  6  7  8  9  10
letter   S  H  I  R  T  G  O  A  T  S
modBy5   1  2  3  4  0  1  2  3  4  0

this way, we know that `G` is the first position letter
in a 5-letter grid.

anything that's 0 is considered as the 5th letter.

```

The `Status` lets us define, for each letter, whether it's in the word, not in the word or in the word but not in the right position.

Sidenote: We could implement this `typedChar` as a list of words instead. Like `[SHIRT, GOATS,...]` etc. But eventually, we'll have to split each word apart into letters to "test" against our master list. Hence, short-circuiting the whole thing to be just a list of characters.

And then, we handle the `KeyPress` event like so:

```elm
KeyPress key ->
		case key of
				Valid char ->
						( { model | typedChars = Array.push (Letter (Array.length model.typedChars + 1) (String.toLower <| String.fromChar char) NotInWord) model.typedChars }, Cmd.none )

				Backspace ->
						( { model
								| typedChars = Array.slice 0 -1 model.typedChars
							}
						, Cmd.none
						)

				_ ->
						( model, Cmd.none )
```

That is, if it's a `Valid` character, we just "push" that into the `typedChars` list. While doing so, we make its `Status` a `NotInWord` by default and add an `index` that we can use to identify its relative position. Also worth noting: we "normalize" the typed letter by lower-casing it and also converting it into a string. Makes things easier to handle.

And if it's `Backspace`, we just remove the last item in `typedChar`.

One last thing to add is a way to "toggle" the status of a `typedChar`.

The idea is this:

- if you click on a char that's `NotInWord`, it should be switched to "in word, but not in the right position" (which is `NotInPosition` in our `Status` type).
- if you click on a char that's `NotInPosition`, it should become marked as `InPosition`. (meaning, it's a letter that's in the right position in the actual word).
- and if you click on an `InPosition` word, it should go back to being `NotInWord`.

```elm
-- this gets added in the `update` function
Toggle index ->
		( { model
				| typedChars =
						Array.indexedMap
								(\idx ltr ->
										if idx == index - 1 then
												{ ltr | status = toggleStatus ltr.status }

										else
												ltr
								)
								model.typedChars
			}
		, Cmd.none
		)

toggleStatus : Status -> Status
toggleStatus status =
    case status of
        NotInWord ->
            NotInPosition

        NotInPosition ->
            InPosition

        InPosition ->
            NotInWord
```

The `view` function is simple.

We simply render the typed characters in a CSS grid of 5-columns each.

```elm
-- helper function to view a character (the badly-named `Word` in our type system)
viewLetter : Letter -> Html Msg
viewLetter ltr =
    let
        bgColor =
            case ltr.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 ltr.index)
        ]
        [ text ltr.char ]

view : Model -> Html Msg
view 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)
```

In the `viewLetter` function, we're using three colors to show the status of the letter like how Wordle does.

If you run this through `elm reactor`, you will see an empty screen and once you start typing letters, they'll start showing up on the screen. And click to toggle their status.

<video width="320" height="240" controls>
<source src="https://i.imgur.com/BfWMr2C.mp4" type="video/mp4" />
</video>