I’m making a spelling bee game with ClojureScript. Here is my code.

(ns spelling-bee.core
(:require
[clojure.string :as str]
[re-frame.core :as rf]
[reagent.core :as ra]
[reagent.dom :as rdom]
[stylefy.core :as stylefy :refer [use-style]]
[stylefy.reagent :as stylefy-reagent]
[spelling-bee.events :as events]
[spelling-bee.words :as words])
(:require-macros
[reagent.core :refer [with-let]]))

(def debug?
^boolean goog.DEBUG)

(set! warn-on-infer false)


;---------- stylefy components ----------
; px vs rem, google
(defn letter-style [letter-validation-sequence]
(case letter-validation-sequence
:required {:color “#4CAF50”}
:valid {:color “#000000”}
:invalid {:color “#AAAAAA” :opacity “0.5”}))



;---------- main page elements ----------

(defn spawn-words-button
“Starts the game with a preset set of words.”
[]
(let [game-started (rf/subscribe [::events/game-started])]
(when-not @game-started
[:button
{:on-click #(rf/dispatch [::events/set-words-and-letters words/word-collection])
:class “button-style”}
“Get Letters!”])))

(defn submit-button
[word]
(let [input-value (rf/subscribe [::events/current-input])]
[:button
{:on-click #(when (seq word)
(println “click!”)
(rf/dispatch [::events/submit-word @input-value]))
:class “button-style”}
“Submit”]))

(defn text-input
“Field for the user to input a word of their choosing.”
[]
(let [input-value (rf/subscribe [::events/current-input])]
[:input {:type “text”
:placeholder “Type here!”
:value @input-value
:on-change #(rf/dispatch [::events/set-current-input (-> % .-target .-value)])
:class “input-style”}]))

(defn shuffle-order-button!
“Shuffles the order of the letters displayed.”
[display-letters]
[:button {:on-click #(rf/dispatch [::events/shuffle-letter-order display-letters])
:class “button-style”}
“Shuffle letters”])



;---------- main page renderer ----------

(defn main-panel []
#{:clj-kondo/ignore [:unresolved-symbol]}
(with-let [
name (rf/subscribe [::events/name])
game-started (rf/subscribe [::events/game-started])
words (rf/subscribe [::events/words])
found-words (rf/subscribe [::events/found-words])
common-letter (rf/subscribe [::events/common-letter])
letters (rf/subscribe [::events/letters])
display-letters (rf/subscribe [::events/display-letters])
current-input (rf/subscribe [::events/current-input])
message (rf/subscribe [::events/message])
score (rf/subscribe [::events/score])
database (rf/subscribe [::events/dbdb])
shake-message? (rf/subscribe [::events/shake-message?])
shake-angry? (rf/subscribe [::events/shake-angry?])]

[:div
[:div {:class “main-style”}
[:h1
"Hello, " @name]
;[:p "debug: "@database]
[:h3 {:class (str “”(when @shake-message? “shake”) (when @shake-angry? “-angry”))} @message]
[spawn-words-button]
(when @game-started
[:div {:class “main-container-style”}
[:div {:class “main-panel-style”}
[:div (use-style {:text-align “center”})
[text-input]
[submit-button @current-input]]

[:p "Common Letter: " (str (first @common-letter))]
[:p "Other Letters: " (str/join “, " @display-letters)]
[:div (use-style {:text-align “center”})
[shuffle-order-button! @display-letters]]
[:h3 “Your score: " @score]]

[:div {:class “side-panel-style”}
[:h3
“Found words:”]
[:ul (for [word (sort @found-words)] ; sort found words into an alphabetical list
[:li word])]]])
]]))


;---------- page load parameters ----------

(defn dev-setup []
(when debug?
(println “dev mode”)))

(defn ^:dev/after-load mount-root []
(rf/clear-subscription-cache!)
(let [root-el (.getElementById js/document “app”)]
(rdom/unmount-component-at-node root-el)
(rdom/render [main-panel] root-el)))

(defn install-global-key-listeners []
(.addEventListener js/window “keydown” events/global-key-handler))

(defn init []
(install-global-key-listeners) ; listen for keypress events
(rf/dispatch-sync [::events/initialize-db]) ; get re-frame atom initialized
(stylefy/init {:dom (stylefy-reagent/init)}) ; set up css
(dev-setup)
(mount-root))


(ns spelling-bee.events
(:require
[clojure.set :as set]
[clojure.string :as str]
[re-frame.core :as rf]))


;---------- our app state atom ----------

(def default-db
{:name “player”
:game-started false
:words #{}
:common-letter #{}
:letters #{}
:display-letters []
:found-words #{}
:current-input “”
:message “Welcome to the Spelling Bee!”
:score 0
:shake-message false
:shake-angry false})

;---------- handlers ----------

(defn global-key-handler [e]
(let [key (.-key e)
input-value (rf/subscribe [::current-input])]
(cond
(re-matches #”[a-zA-Z]” key)
(rf/dispatch [::append-current-input (str key)])

(= key “Enter”)
(rf/dispatch [::submit-word @input-value])

(= key “Backspace”)
(let [subtract-letter #(subs % 0 (dec (count %)))]
(rf/dispatch [::set-current-input (subtract-letter @input-value)]))

:else
nil)))
; remove subscribe, do in functions

;---------- various functions ----------

;; Later this can be substituted with a database call to pull a list of words.


(defn get-unique-letter-collection [word-set]
(-> word-set
vec
str/join
seq
set))

(defn find-common-letter [word-set]
(reduce
set/intersection
(map set (seq word-set))))



(defn validate-word
“Checks the given word against the current word list and letter set to see if it is valid. Gives the following keywords as a result.

:submit-ok :too-short :invalid :no-common :not-in-list :other”
[word word-list letters common-letter]
(cond
(contains? word-list word) :submit-ok ; first check if the word is in the word collection
(> 4 (count (seq word))) :too-short ; check length, notify if less than 3 letters
(not (every? letters (set word))) :invalid ; check if every letter in the word is in letters set
(not (contains? (set word) (first common-letter))) :no-common ; if it does not contain the common letter
(contains? (set word) (first common-letter)) :not-in-list ; then check if the word at least contains common letter
:else :other)) ; generic if it somehow manages to not match one of the above

(defn validate-letter [letter letters common-letter]
(cond
(= letter (str (first common-letter))) :required
(contains? (set letters) letter) :valid
:else :invalid))

(defn calculate-points [word letters]
(cond
(= (get-unique-letter-collection word) (set letters)) (+ (count (seq word)) 7)
(= (count (seq word)) 4) (int 1)
:else (count (seq word))))

;; (map #(validate-letter #{}) (seq “arroyo”))

;---------- subscriptions to data from app state ----------

(rf/reg-sub ::name
(fn [db]
(:name db)))

(rf/reg-sub ::game-started
(fn [db]
(:game-started db)))

(rf/reg-sub ::words
(fn [db]
(:words db)))

(rf/reg-sub ::found-words
(fn [db]
(:found-words db)))

(rf/reg-sub ::common-letter
(fn [db]
(:common-letter db)))

(rf/reg-sub ::letters
(fn [db]
(:letters db)))

(rf/reg-sub ::display-letters
(fn [db]
(:display-letters db)))

(rf/reg-sub ::current-input
(fn [db]
(:current-input db)))

(rf/reg-sub ::message
(fn [db]
(:message db)))

(rf/reg-sub ::score
(fn [db]
(:score db)))

(rf/reg-sub ::dbdb
(fn [db]
db))

(rf/reg-sub ::shake-message?
(fn [db]
(:shake-message db)))

(rf/reg-sub ::shake-angry?
(fn [db]
(:shake-angry db)))


;---------- events ----------

(rf/reg-event-db ::initialize-db
(fn [ ]
default-db))

(rf/reg-event-db ::set-words-and-letters
(fn [db [ word-set]]
(let [common-letter (find-common-letter word-set)
letter-coll (get-unique-letter-collection word-set)]
(assoc db :words word-set
:common-letter common-letter
:letters letter-coll
:display-letters (shuffle (vec (remove common-letter letter-coll)))
:game-started true))))

(rf/reg-event-db ::set-current-input
(fn [db [_ input-value]]
(assoc db :current-input input-value)))

(rf/reg-event-db ::append-current-input
(fn [db [_ input-value]]
(update db :current-input str input-value)))

(rf/reg-event-db ::shuffle-letter-order
(fn [db [_ display-letters]]
(assoc db :display-letters (shuffle display-letters))))

(rf/reg-event-db ::reset-shake-message
(fn [db ]
(assoc db :shake-message false :shake-angry false)))

(rf/reg-event-db ::submit-word
(fn [db [ word]]
(let [letters (:letters db)
common-letter (:common-letter db)
words (:words db)
point-val (calculate-points word letters)
submit (partial assoc db :current-input “” :message)]
(js/setTimeout #(rf/dispatch [::reset-shake-message]) 500) ; preemptively set a timeout to kill any shaking
(case (validate-word word words letters common-letter)
:submit-ok (if (contains? (:found-words db) word)
(submit “You’ve already found that word!”)
(-> db
(update :found-words conj word)
(update :score + point-val)
(assoc :current-input “” :message (str "Great job! You found " word ", worth a score of " point-val “!”)))) ; add the valid word to found words
:too-short (submit “Only words with 4 letters or more count.”)
:not-in-list (submit (str “Sorry, " word " isn’t in the word list today.”))
:no-common (submit “Nice try, but the word needs to contain the common letter.” :shake-message true)
:invalid (submit “All letters in the word must be from the given letter set.” :shake-message true :shake-angry true)
:other (submit “Try again.”)))))
; use reg-event-fx to dispatch further event to clear input





Currently the letters to be pressed are only able to be typed on the keyboard. I want the valid letters to be represented as clickable buttons in a hexagonal layout.