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
- Babashka's task runner for site building.
- Selmer for templating.
- Hiccup for html.
- Scittle with 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.
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. 😄
The algorithm
The algorithm is a fancy filter/remove predicate at bottom.
(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
- green at the given index
- yellow, but not at the given index
- 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))))
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]))
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)))
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}?
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))
This get's pretty close to what we want.
(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.
(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.
(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. 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]))
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.
Top comments (3)
Before jumping to performance conclusions about scittle, I think it would be fair to do some more work to find out if the performance lag is attributable to the organization in the code, e.g. by comparing your existing solution in scittle vs normal CLJS.
See here for another scittle wordle:
babashka.org/scittle/wordle.html
Btw, you're also using a very old version of scittle from a year or so ago.
There have been significant performance improvements in SCI in the last year which also have been published to scittle.
github.com/babashka/scittle/releas...
Fixed. Thanks for the feedback.
Looks like I have a follow-up article to write.