Skip to content

Instantly share code, notes, and snippets.

@borkdude
Last active January 28, 2026 13:19
Show Gist options
  • Select an option

  • Save borkdude/21f8e8996d5d20044f3f1c265ab7cc48 to your computer and use it in GitHub Desktop.

Select an option

Save borkdude/21f8e8996d5d20044f3f1c265ab7cc48 to your computer and use it in GitHub Desktop.
snake in babashka
#!/usr/bin/env bb
(ns snake
"Snake game TUI using JLine terminal library"
(:import [org.jline.terminal TerminalBuilder]
[org.jline.utils InfoCmp$Capability]))
(def size 20) ;; grid size (20x20)
(defonce state
(atom {:snake [[5 10] [4 10] [3 10]] ; initial snake body
:dir [1 0] ; moving right
:food [15 10] ; initial food
:alive? true
:score 0}))
(defn random-food [snake]
(let [food [(rand-int size) (rand-int size)]]
(if (some #(= food %) snake)
(recur snake)
food)))
(defn move-snake []
(swap! state
(fn [{:keys [snake dir food alive?] :as st}]
(if (not alive?)
st
(let [head [(+ (first (first snake)) (first dir))
(+ (second (first snake)) (second dir))]
ate? (= head food)
new-snake (vec (cons head (if ate? snake (butlast snake))))
[x y] head
hit-wall? (or (< x 0) (< y 0) (>= x size) (>= y size))
hit-self? (some #(= head %) (rest new-snake))]
(cond
hit-wall? (assoc st :alive? false)
hit-self? (assoc st :alive? false)
ate? (-> st
(assoc :snake new-snake)
(assoc :food (random-food new-snake))
(update :score inc))
:else (assoc st :snake new-snake)))))))
(defn handle-key [key]
(swap! state update :dir
(fn [dir]
(case key
(:up \k) (if (= dir [0 1]) dir [0 -1])
(:down \j) (if (= dir [0 -1]) dir [0 1])
(:left \h) (if (= dir [1 0]) dir [-1 0])
(:right \l) (if (= dir [-1 0]) dir [1 0])
dir))))
(defn clear-screen [terminal]
(.puts terminal InfoCmp$Capability/clear_screen (object-array 0))
(.flush terminal))
(defn move-cursor [writer row col]
(.write writer (str "\u001b[" row ";" col "H")))
(defn render [terminal]
(let [writer (.writer terminal)
{:keys [snake food alive? score]} @state]
;; Move cursor to top-left
(move-cursor writer 1 1)
;; Draw top border
(.write writer (str "+" (apply str (repeat (* size 2) "-")) "+\n"))
;; Draw grid
(doseq [y (range size)]
(.write writer "|")
(doseq [x (range size)]
(let [pos [x y]
char (cond
(some #(= pos %) snake) (if (= pos (first snake))
"\u001b[92m██\u001b[0m" ; bright green head
"\u001b[32m██\u001b[0m") ; green body
(= pos food) "\u001b[31m██\u001b[0m" ; red food
:else " ")]
(.write writer char)))
(.write writer "|\n"))
;; Draw bottom border
(.write writer (str "+" (apply str (repeat (* size 2) "-")) "+\n"))
;; Draw score and instructions
(.write writer (str "\nScore: " score "\n"))
(.write writer "Controls: Arrow keys or hjkl | q to quit\n")
;; Game over message
(when (not alive?)
(.write writer "\n*** GAME OVER! Press 'r' to restart or 'q' to quit ***\n"))
(.flush writer)))
(defn restart-game []
(reset! state {:snake [[5 10] [4 10] [3 10]]
:dir [1 0]
:food [15 10]
:alive? true
:score 0}))
(defn read-key [reader]
(let [c (.read reader 1)]
(when (pos? c)
(cond
;; ESC sequences
(= c 27)
(let [c2 (.read reader 50)]
(when (pos? c2)
(case c2
;; ESC [ A/B/C/D (Unix/ANSI)
91 (let [c3 (.read reader 50)]
(case c3
65 :up
66 :down
67 :right
68 :left
nil))
;; ESC O A/B/C/D (Application mode / Windows)
79 (let [c3 (.read reader 50)]
(case c3
65 :up
66 :down
67 :right
68 :left
nil))
nil)))
;; Regular character
:else (char c)))))
(defn -main [& _args]
(let [terminal (-> (TerminalBuilder/builder)
(.system true)
(.ffm true)
(.build))
reader (.reader terminal)]
(try
;; Enter raw mode for unbuffered input
(.enterRawMode terminal)
(clear-screen terminal)
(render terminal)
;; Game loop
(loop [last-move (System/currentTimeMillis)
running true]
(when running
(let [now (System/currentTimeMillis)
elapsed (- now last-move)
{:keys [alive?]} @state
key (read-key reader)]
;; Handle input
(cond
;; Quit
(= key \q)
(clear-screen terminal)
;; Restart when dead
(and (not alive?) (= key \r))
(do (restart-game)
(clear-screen terminal)
(render terminal)
(recur now true))
;; Direction change
key
(do (handle-key key)
(recur last-move true))
;; Move snake on timer
(and alive? (>= elapsed 150))
(do (move-snake)
(render terminal)
(recur now true))
;; Keep looping
:else
(do (Thread/sleep 10)
(recur last-move true))))))
(finally
(.close terminal)))))
#_(when (= *file* (System/getProperty "babashka.file"))
(-main))
(-main)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment