Created
January 17, 2017 09:14
-
-
Save terry3/26eebc0efca50b737f7cbfefa4f16420 to your computer and use it in GitHub Desktop.
Snake game in clojure v1.8
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
; 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))) | |
(import java.awt.event.KeyEvent) | |
(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 (. KeyEvent VK_LEFT)) :left | |
(= key-code (. KeyEvent VK_RIGHT)) :right | |
(= key-code (. KeyEvent VK_UP)) :up | |
(= key-code (. KeyEvent 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)) | |
(main) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment