This is the Clojure source code, written in an intentional style, for a simple game. It was originally written by Abhishek Reddy. It is being presented as an example of good Clojure coding style. Feedback is welcomed at r.mark.volkmann@gmail.com.
To run this, save the code in a file named "snake.clj" and run "clj snake.clj". Of course this assumes you've creates the clj script for running Clojure code or a REPL.
; This is a Swing-based game where the arrow keys to guide ; a snake to apples. Each time the snake eats an apple it ; grows and a new apple appears in a random location. ; If the head of the snake hits its body, you lose. ; If the snake grows to a length of 10, you win. ; In either case the game starts over with a new, baby snake. ; ; This was originally written by Abhishek Reddy. ; Mark Volkmann rewrote it in an attempt to make it easier to understand. (ns com.ociweb.snake (:import (java.awt Color Dimension) (java.awt.event KeyListener) (javax.swing JFrame JOptionPane JPanel)) (:use clojure.contrib.import-static)) (import-static java.awt.event.KeyEvent VK_LEFT VK_RIGHT VK_UP VK_DOWN) (defstruct cell-struct :x :y) (defstruct snake-struct :body :direction) (defstruct game-struct :panel :cell-size :length-to-win :ms-per-move :apple :snake) (defn board-dimensions [panel cell-size] (let [size (.getPreferredSize panel)] [(quot (.getWidth size) cell-size) (quot (.getHeight size) cell-size)])) (defn create-center-cell [width height] (struct cell-struct (quot width 2) (quot height 2))) (defn create-random-cell [width height] (struct cell-struct (rand-int (- width 1)) (rand-int (- height 1)))) (defn create-snake [width height] (let [head (create-center-cell width height) body (list head)] (struct snake-struct body :right))) (defn create-game [panel cell-size] (let [length-to-win 10 ms-per-move 50 [width height] (board-dimensions panel cell-size) apple (create-random-cell width height) snake (create-snake width height)] (struct game-struct panel cell-size length-to-win ms-per-move apple snake))) (defn paint-cell [panel color cell-size {x :x y :y}] (let [graphics (.getGraphics panel)] (.setColor graphics color) (.fillRect graphics (* x cell-size) (* y cell-size) cell-size cell-size))) (defn erase-cell [game cell] (let [panel (game :panel) color (.getBackground panel) cell-size (game :cell-size)] (paint-cell panel color cell-size cell))) (defn erase-apple [game] (let [apple (game :apple)] (erase-cell game apple))) (defn erase-snake [game] (doseq [cell ((game :snake) :body)] (erase-cell game cell))) (defn paint-apple [panel cell-size apple] (paint-cell panel Color/RED cell-size apple)) (defn paint-snake [panel cell-size snake] ; We only need to paint the head because ; the rest will have been already painted. (let [head (first (snake :body))] (paint-cell panel Color/GREEN cell-size head))) (defn paint-game [game] (let [panel (game :panel) cell-size (game :cell-size)] (paint-apple panel cell-size (game :apple)) (paint-snake panel cell-size (game :snake)))) (defn new-apple [game] (let [panel (game :panel) cell-size (game :cell-size) [width height] (board-dimensions panel cell-size)] (erase-apple game) (create-random-cell width height))) (defn delta "Gets a vector containing dx and dy values for a given direction." [direction] (direction {:left [-1 0], :right [1 0], :up [0 -1], :down [0 1]})) (defn new-direction "Returns the snake's direction, either the current direction or a new one if a board edge was reached." [game] (let [snake (game :snake) direction (snake :direction) head (first (snake :body)) x (head :x) y (head :y) panel (game :panel) cell-size (game :cell-size) [width height] (board-dimensions panel cell-size) at-left (= x 0) at-right (= x (- width 1)) at-top (= y 0) at-bottom (= y (- height 1))] ; Turn clockwise when a board edge is reached ; unless that would result in going off the board. (cond (and (= direction :up) at-top) (if at-right :left :right) (and (= direction :right) at-right) (if at-bottom :up :down) (and (= direction :down) at-bottom) (if at-left :right :left) (and (= direction :left) at-left) (if at-top :down :up) true direction))) (defn same-or-adjacent-cell? [cell1 cell2] (let [dx (Math/abs (- (cell1 :x) (cell2 :x))) dy (Math/abs (- (cell1 :y) (cell2 :y)))] (and (<= dx 1) (<= dy 1)))) (defn eat-apple? [game] (let [apple (game :apple) snake (game :snake) head (first (snake :body))] (same-or-adjacent-cell? head apple))) (defn remove-tail [game body] (let [tail (last body)] (erase-cell game tail) (butlast body))) (defn move-snake [game grow] "Moves the snake and returns a new snake-struct. The snake grows it by one cell if 'grow' is true." (let [direction (new-direction game) [dx dy] (delta direction) snake (game :snake) body (snake :body) head (first body) x (head :x) y (head :y) new-head (struct cell-struct (+ x dx) (+ y dy)) body (cons new-head body) body (if grow body (remove-tail game body))] (struct snake-struct body direction))) (defn get-key-direction "Gets a keyword that describes the direction associated with a given key code." [key-code] (cond (= key-code VK_LEFT) :left (= key-code VK_RIGHT) :right (= key-code VK_UP) :up (= key-code VK_DOWN) :down true nil)) (defn snake-with-key-direction [snake key-code-atom] (let [key-code @key-code-atom key-direction (get-key-direction key-code) current (snake :direction) ; Don't let the snake double back on itself. valid-change (cond (= key-direction nil) false (= key-direction :left) (not= current :right) (= key-direction :right) (not= current :left) (= key-direction :up) (not= current :down) (= key-direction :down) (not= current :up) true true)] (if valid-change (do (compare-and-set! key-code-atom key-code nil) (assoc snake :direction key-direction)) snake))) (defn head-overlaps-body? [body] (let [head (first body)] (some #(= % head) (rest body)))) (defn restart-game [game] (erase-apple game) (erase-snake game) (create-game (game :panel) (game :cell-size))) (defn new-game [game message] (let [panel (game :panel) top (.getTopLevelAncestor panel)] (JOptionPane/showMessageDialog top message) (restart-game game))) (defn win? [game] (let [snake (game :snake) body (snake :body)] (= (count body) (game :length-to-win)))) (defn lose? [game] (let [snake (game :snake) body (snake :body)] (head-overlaps-body? body))) (defn step [game key-code-atom] (let [eat (eat-apple? game) snake (snake-with-key-direction (game :snake) key-code-atom) game (assoc game :snake snake) game (if eat (assoc game :apple (new-apple game)) game) snake (move-snake game eat)] (cond (lose? game) (new-game game "You killed the snake!") (win? game) (new-game game "You win!") true (assoc game :snake snake)))) (defn create-panel [width height key-code-atom] (proxy [JPanel KeyListener] [] ; superclass constructor arguments (getPreferredSize [] (Dimension. width height)) (keyPressed [e] (compare-and-set! key-code-atom @key-code-atom (.getKeyCode e))) (keyReleased [e]) ; do nothing (keyTyped [e]) ; do nothing )) (defn configure-gui [frame panel] (doto panel (.setFocusable true) ; won't generate key events without this (.addKeyListener panel)) (doto frame (.add panel) (.pack) (.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE) (.setVisible true))) (defn main [] (let [frame (JFrame. "Snake") width 30 height 30 cell-size 10 key-code-atom (atom nil) panel-width (* width cell-size) panel-height (* height cell-size) panel (create-panel panel-width panel-height key-code-atom) first-game (create-game panel cell-size)] (configure-gui frame panel) (loop [game first-game] (paint-game game) (Thread/sleep (game :ms-per-move)) (recur (step game key-code-atom))))) ; Only run the application if this is being run as a script, ; not if loaded in a REPL with load-file. ; When run as a script, the path to this file ; will be a command-line argument. (if *command-line-args* (main))