Using scittle to solve wordle

crinklywrappr

Daniel Fitzpatrick

Posted on April 18, 2022

Using scittle to solve wordle

A co-worker wrote a web app to solve Wordle puzzles, and I thought it would be fun to port it to Scittle. It works, but not super well (as you shall see). Use his app instead.

High-level overview (tl;dr)

I used the following tools for static site development

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.

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. 😄

App screenshot

The algorithm

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

(defn filter-words [words]
  (remove unfit-word? words))
Enter fullscreen mode Exit fullscreen mode

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.

(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))))
Enter fullscreen mode Exit fullscreen mode

I have modeled the individual lists to facilitate this function.

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

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

(def greenlist (r/atom [nil nil nil nil nil]))
Enter fullscreen mode Exit fullscreen mode

unfit-word? can now be written:

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

(defn unfit-word? [indexed-yellows word]
  (some unfit-letter? (map-indexed vector word)))
Enter fullscreen mode Exit fullscreen mode

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.

;; 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}?
Enter fullscreen mode Exit fullscreen mode

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

(defn index-yellow-letters []
  (reduce-kv
   (fn [a k v]
     (reduce
      (fn [ax bx]
        (update ax bx conj k))
      a v))
   {} @yellowlist))
Enter fullscreen mode Exit fullscreen mode

This get's pretty close to what we want.

(reset! yellowlist [#{t} #{} #{} #{t} #{}])
(index-yellow-letters) ;=> {\t (0 3)}
Enter fullscreen mode Exit fullscreen mode

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.

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

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

(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))
Enter fullscreen mode Exit fullscreen mode

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. 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. 😄

The bad

If I want to write a "general use" static site generator, I will likely need to add many tags. yogthos/selmver#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:

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

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

(def greenlist (r/atom [nil nil nil nil nil]))
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
crinklywrappr
Daniel Fitzpatrick

Posted on April 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Using scittle to solve wordle
clojure Using scittle to solve wordle

April 18, 2022