Using scittle to solve wordle
Tags: clojure, javascript, react
Published: 2022-04-18 16:08:21

A [co-worker](https://github.com/vinmaster) wrote a web app to solve Wordle puzzles, and I thought it would be fun to port it to [Scittle](https://babashka.org/scittle/).  It works, but not super well (as you shall see).  Use his [app](https://wordle-solve.netlify.app/) instead.

## High-level overview (tl;dr)

I used the following tools for static site development
- Babashka's [task runner](https://book.babashka.org/#tasks) for site building.
- [Selmer](https://github.com/yogthos/Selmer) for templating.
- [Hiccup](https://github.com/weavejester/hiccup) for html.
- [Scittle](https://babashka.org/scittle/) with [reagent](https://github.com/reagent-project/reagent) for frontend logic.

Everything worked well, but Scittle seemed laggy.  The lag might be attributable to a mistake that triggers too many redraws of some components.  Readers are welcome to submit a PR to fix it.

- [Project](https://github.com/crinklywrappr/wordlsolver/tree/static)
- [App](https://musing-darwin-a9ac20.netlify.app/)

Rather than steal Vincent's algorithm, I wrote my solver from scratch.  I cannot say the same for the window-dressing: I have blatantly stolen his CSS.  :smile:

![App screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r4g2gi538h7poc5bwuh4.png)

## The algorithm

The algorithm is a fancy filter/remove predicate at bottom.

```clojure
(defn filter-words [words]
  (remove unfit-word? words))
```

An excellent first step would be determining which letters are possible at a given index.  A letter is allowable if it's
1. green at the given index
2. yellow, but not at the given index
3. missing from the blacklist

These possibilities are mutually exclusive.

```clojure
(defn get-possible-letters
  "Returns a set of allowable letters for a given index"
  [index]
  (if-let [letter (get @greenlist index)]
    #{letter}
    (set/difference alphas @blacklist (get @yellowlist index))))
```

I have modeled the individual lists to facilitate this function.

```clojure
(def blacklist (r/atom #{}))

(def yellowlist (r/atom [#{} #{} #{} #{} #{}]))

(def greenlist (r/atom [nil nil nil nil nil]))
```

`unfit-word?` can now be written:

```clojure
(defn unfit-letter? [[index letter]]
  (nil? ((get-possible-letters index) letter)))

(defn unfit-word? [indexed-yellows word]
  (some unfit-letter? (map-indexed vector word)))
```

This code represents **_most_** of the work required, but there's an important piece missing.  If a letter is in the yellow list, then it **_must_** be part of the word.  But, unfortunately, we haven't guaranteed that.

If we could transform a word into a set containing only the letters that aren't in a given set of indices, then it would be possible to perform this check.

Imagine that we have the word "truth," and both t's are yellow.  In our model that looks like eg `[#{t} #{} #{} #{t} #{}]`.  The word "about" fits the criteria.  Let's work this backward.

```clojure
;; remove the indices specified by the yellowlist and see if 't' is in the resultset
(#{\b \o \t} \t) ;=> \t
(#{\b \o \t} \x) ;=> nil

;; how do we get #{\b \o \t}?
;; there are many ways but let's try this one
(disj (set (replace-idx {0 nil 3 nil} (vec "about"))) nil)

;; `replace-idx` doesn't exist in scittle.
;; We could write it but let's try this instead
(reduce-kv
 (fn [a k v]
   (if (#{0 3} k)
     a (conj a v)))
 #{} (vec "about"))

;; how do we go from [#{t} #{} #{} #{t} #{}] to #{0 3}?
```

I will define a function called `index-yellow-letters`.

```clojure
(defn index-yellow-letters []
  (reduce-kv
   (fn [a k v]
     (reduce
      (fn [ax bx]
        (update ax bx conj k))
      a v))
   {} @yellowlist))
```

This get's pretty close to what we want.

```clojure
(reset! yellowlist [#{t} #{} #{} #{t} #{}])
(index-yellow-letters) ;=> {\t (0 3)}
```

Next, let's define a function called `unfit-subword?`, where 'subword' refers to a set of letters, e.g. ` #{\b \o \t}` in the previous example.  This function will encapsulate the rest of the logic we worked through earlier.

```clojure
(defn unfit-subword? [word [letter ix]]
  (nil?
   (reduce-kv
    (fn [a k v]
      (if ((set ix) k)
        a (conj a v)))
    #{} (vec word))
   letter))
```

Finally, redefine `unfit-word?` & `filter-words` to take this new logic into account.

```clojure
(defn unfit-word? [indexed-yellows word]
  (or (some unfit-letter? (map-indexed vector word))
      (some (partial unfit-subword? word) indexed-yellows)))

(defn filter-words [words]
  (remove (partial unfit-word? (index-yellow-letters)) words))
```

## The good

Using Selmer & Hiccup for static site construction (and Babashka's task runner for running it) worked so marvelously that I want to use them to write a fully featured static site generator.

Shout-out to [miniserve](https://github.com/svenstaro/miniserve).  I didn't need it for this project because I wanted to generate a single file.  If I had generated multiple output files, Miniserve would have been *very* useful for testing.  :smile:

## The bad

If I want to write a "general use" static site generator, I will likely need to add many tags.  [yogthos/selmver#278](https://github.com/yogthos/Selmer/issues/278) for reference.

## The ugly

Scittle is super cool but under-performing in its current state.  You probably noticed some lag when toggling colors.

That might be my fault, though.  I chose to model the state like this:

```clojure
(def blacklist (r/atom #{}))

(def yellowlist (r/atom [#{} #{} #{} #{} #{}]))

(def greenlist (r/atom [nil nil nil nil nil]))
```

As you can imagine, a color toggle alters all three of these "ratoms."  This behavior means that, unless there is some debouncing under the covers, color toggling triggers more redraws than necessary.  I will happily accept a PR if you think this is the problem.