Last active
January 28, 2026 13:19
-
-
Save borkdude/21f8e8996d5d20044f3f1c265ab7cc48 to your computer and use it in GitHub Desktop.
snake in babashka
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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