Snake in ~100 Lines of ClojureScript
Source: theronic/cv/src/cv/snake.cljs · theronic/cljs-snake · this port: · the rules:
← → turn↑ ↓ ← → or W S A D steer · Space pauses · Enter restarts · Esc leaves
Warming the interpreter…
Source
Snake.core, all 186 lines, as this machine runs it
(ns snake.core
"Snake in ~100 lines of ClojureScript (if you exclude UI, comments and helpers). It's
running in this LISP machine, so you can eval `(snake/turn! :left)` or `(snake/turn! :right)` in the
REPL to control the snake :)."
(:require
[pt.host :as host]
[pt.listener :as listener]))
(def initial-state
{:paused? false
:width 12
:height 12
:position [6 8]
:history ()
:pill-density 0.02
:pills #{}
:velocity [1 0]
:size 3
:queue [:left]
:dead? false
:interval 250})
(defonce !state (atom initial-state))
(defonce !timer (atom nil))
(defonce !mode (atom :relative)) ; how keys and buttons steer, :relative or :absolute (owner, 2026-09-25)
(defn next-state [{:keys [position size velocity queue pills pill-density width height history] :as state}]
(let [snake (set (take size history))
found-pill? (pills position)
[dx dy] velocity
direction (first queue)
turned (case direction ; 90-degree rotation matrices
:left [dy (- dx)]
:right [(- dy) dx]
[dx dy])
ahead #(mapv mod (map + % position) [width height])
new-vel (if (snake (ahead turned)) [dx dy] turned) ; turn protection: no turning into the body
new-pos (ahead new-vel)
pills (if found-pill? (disj pills position) pills)
missing-pills (- (* pill-density width height) (count pills))
new-pills (take missing-pills (repeatedly #(vector (rand-int width) (rand-int height))))]
(assoc state
:dead? (if (snake new-pos) true false)
:queue (drop 1 queue)
:velocity new-vel
:history (take size (conj history position))
:position new-pos
:interval (- 250 (* 3 size))
:pills (into pills new-pills)
:size (if found-pill? (inc size) size))))
(defn- step
"One tick of the original rules, then :history compacted into a plain list. Each tick's lazy
`take` otherwise holds on to every earlier generation, a slow leak in long games."
[state]
(update (next-state state) :history #(apply list %)))
(def heads {[1 0] "▶" [-1 0] "◀" [0 -1] "▲" [0 1] "▼"})
(defn render
"The board as rows of glyphs: head shows its heading, █ body, ◆ pill, · empty."
[{:keys [pills size width height history position velocity]}]
(let [worm (set (take size history))]
(vec (for [y (range height)]
(apply str (for [x (range width)]
(let [pos [x y]]
(cond (= position pos) (heads velocity "@")
(worm pos) "█"
(pills pos) "◆"
:else "·"))))))))
(defn- draw! []
(let [s @!state]
(host/tty-draw! (render s) {:score (- (:size s) (:size initial-state))
:dead (:dead? s)
:paused (:paused? s)
:state (listener/pretty s)})))
(defn- loop! []
(reset! !timer
(js/setTimeout (fn []
(when-not (or (:dead? @!state) (:paused? @!state))
(swap! !state step))
(draw!)
(loop!))
(:interval @!state))))
(defn turn! "Queue a relative turn, :left or :right." [dir] (swap! !state update :queue conj dir) dir)
(defn tick! "Advance the game one step." [] (swap! !state step) (draw!) (:position @!state))
(defn grow! "Grow the snake by one." [] (swap! !state update :size inc) (draw!) (:size @!state))
(defn shrink! "Shrink the snake by one." [] (swap! !state update :size dec) (draw!) (:size @!state))
(defn faster! "Shorten the tick, as the original's Faster button did." [] (swap! !state update :interval - 15) (:interval @!state))
(defn slower! "Lengthen the tick, as the original's Slower button did." [] (swap! !state update :interval + 15) (:interval @!state))
(defn pause! "Pause or resume the game." [] (swap! !state update :paused? not) (draw!) (:paused? @!state))
(defn new-game! "Start over." [] (reset! !state initial-state) (draw!) :new-game)
(def directions "The absolute directions, as velocities." {:up [0 -1] :down [0 1] :left [-1 0] :right [1 0]})
(defn direction!
"Head :up, :down, :left or :right (owner, 2026-09-25) by queueing the turn that gets there
from the heading the queued turns leave. Straight back would turn the snake in on itself, so
it is refused, like the heading it already has: nil."
[dir]
(let [[dx dy] (reduce (fn [[x y] t] (case t :left [y (- x)] :right [(- y) x] [x y]))
(:velocity @!state)
(:queue @!state))]
(condp = (directions dir)
[dy (- dx)] (do (turn! :left) dir)
[(- dy) dx] (do (turn! :right) dir)
nil)))
(defn mode!
"Steer with :relative turns, Left and Right, or :absolute directions, the arrows or WASD
(owner, 2026-09-25). It changes mid-game, and /snake puts it in the address to share."
[m]
(if (#{:relative :absolute} m)
(do (reset! !mode m) (host/snake-mode! (name m)) m)
(throw (ex-info (str "Unknown mode " (pr-str m)) {:pt/hint "try (snake/mode! :relative) or (snake/mode! :absolute)"}))))
(defn- elsewhere? "A key typed into a field, or pressed with a modifier, is the page's." [e]
(or (#{"TEXTAREA" "INPUT" "SELECT"} (.. e -target -tagName)) (.-metaKey e) (.-ctrlKey e) (.-altKey e)))
(defn- press!
"Run a key's or a button's command, such as (snake/turn! :left), and show it in the Listener as if
typed. It runs at once; the Listener only records it."
[[cmd & args :as form]]
(let [v (apply ({'snake/turn! turn! 'snake/direction! direction! 'snake/pause! pause! 'snake/new-game! new-game!} cmd) args)]
(host/shown! (pr-str form) (pr-str v))
v))
(def ^:private keymap
"Each mode's keys, lower-cased, and the commands they run."
(let [steer #(zipmap % (for [d [:up :down :left :right]] (list 'snake/direction! d)))]
{:relative {"arrowleft" '(snake/turn! :left) "arrowright" '(snake/turn! :right)}
:absolute (merge (steer ["arrowup" "arrowdown" "arrowleft" "arrowright"]) (steer ["w" "s" "a" "d"]))}))
(defn- handle-key!
"The mode's keys steer; Space or Enter on the board pauses. Anywhere else, Space pauses,
Enter restarts a finished game and Escape leaves."
[e]
(when-not (elsewhere? e)
(let [k (.-key e)
form (get-in keymap [@!mode (.toLowerCase k)])]
(cond
form (do (.preventDefault e) (press! form))
(and (= "tty" (.. e -target -id)) (#{" " "Enter"} k)) (do (.preventDefault e) (press! '(snake/pause!)))
(= k " ") (do (.preventDefault e) (press! '(snake/pause!)))
(= k "Enter") (when (:dead? @!state) (press! '(snake/new-game!)))
(= k "Escape") (.back js/PT87)))))
(defn- pressed
"The command a [data-turn] or [data-dir] button asks for, such as (snake/direction! :up)."
[e]
(when-let [b (some-> (.-target e) (.closest "[data-turn], [data-dir]"))]
(if-let [t (.. b -dataset -turn)]
(list 'snake/turn! (keyword t))
(list 'snake/direction! (keyword (.. b -dataset -dir))))))
(defn- handle-tap!
"Steer as a finger lands, before any double-tap zoom; a press on the board pauses or resumes."
[e]
(if-let [form (pressed e)]
(do (.preventDefault e) (press! form))
(when (some-> (.-target e) (.closest "#tty")) (press! '(snake/pause!)))))
(defn- handle-click!
"A button pressed from the keyboard (a click with detail 0); pointers pressed it on pointerdown."
[e]
(when (zero? (.-detail e)) (some-> (pressed e) press!)))
(defn stop! "Stop the game loop and release the keyboard and turn buttons." []
(some-> @!timer js/clearTimeout)
(reset! !timer nil)
(.removeEventListener js/window "keydown" handle-key!)
(.removeEventListener js/document "pointerdown" handle-tap!)
(.removeEventListener js/document "click" handle-click!))
(defn start! "Start a new game on the screen's terminal." []
(stop!)
(reset! !state initial-state)
(.addEventListener js/window "keydown" handle-key!)
(.addEventListener js/document "pointerdown" handle-tap!)
(.addEventListener js/document "click" handle-click!)
(host/on-leave! stop!)
(draw!)
(loop!))