basic-snakeA basic snake game only use traditional html method. (Without react.js, vue.js ...etc) dependencies
| (this space intentionally left almost blank) | |||
(ns basic-snake.app
(:require [goog.dom :as dom]
[goog.events :as events]
[goog.events.KeyCodes]
[goog.events.EventType]
[clojure.pprint :refer [pprint]])) | ||||
States | ||||
(def state
(atom { ;; canvas object
:canvas/element (dom/getElement "canvas")
:canvas/ctx (-> (dom/getElement "canvas") (.getContext "2d"))
:canvas/background-color "white" ; default canvas color (background)
:canvas/width 640
:canvas/height 480
;; snake object
:snake/body '([0 0] [1 0] [2 0]) ; [x y]
:snake/direction [0 1] ; default direction, see `keycode->direction`
:snake/width 32 ; 640 / 20
:snake/height 24 ; 480 / 20
:snake/border 2 ; border size
:snake/body-color "lime" ; snake's body color
:snake/food nil ; when `nil`, regenerate it
:snake/food-color "red" ; the color of food
:snake/alive true ; when `false`, stop game loop
})) | ||||
Helper functions | ||||
(defn axis-add [[x1 y1] [x2 y2]] [(+ x1 x2) (+ y1 y2)]) | ||||
(defn axis-equal? [[x1 y1] [x2 y2]] (and (= x1 x2) (= y1 y2))) | ||||
Print current game state on console.(For debugging purpose) | (defn print-state [] (.log js/console "-------------------------------") (.log js/console (with-out-str (pprint @state))) (.log js/console "-------------------------------\n")) | |||
Canvas functions | ||||
Draw the point on canvas according to snake's width/height. | (defn draw
[[x y] color]
(let [{:keys [:canvas/ctx :snake/width :snake/height :snake/border]} @state]
(aset ctx "fillStyle" color) ;; (set! (.-fillStyle ctx) color)
(.fillRect ctx
(* x width)
(* y height)
(- width border)
(- height border)))) | |||
Resize the canvas according to state. | (defn resize-canvas
[]
(let [{:keys [:canvas/element :canvas/width :canvas/height]} @state]
(.setAttribute element "width" width)
(.setAttribute element "height" height))) | |||
Game's functions | ||||
Convert javascript's keycode to direction array. | (defn keycode->direction
[keycode]
(get {goog.events.KeyCodes.UP [ 0 -1] ; code: 38
goog.events.KeyCodes.DOWN [ 0 1] ; code: 40
goog.events.KeyCodes.LEFT [-1 0] ; code: 37
goog.events.KeyCodes.RIGHT [ 1 0]} ; code: 39
keycode nil)) | |||
Detect two direction array are opposite direction or not. | (defn opposite-direction? [dir1 dir2] (= [0 0] (axis-add dir1 dir2))) | |||
The keydown event handler. | (defn on-keydown
[event]
(let [{:keys [:snake/direction]} @state
new-direction (keycode->direction (.-keyCode event))]
;; We only handle direction exist condition
(when new-direction
;; When two direction are not opposite direction, save new direction
(when-not (opposite-direction? new-direction direction)
(swap! state assoc-in [:snake/direction] new-direction))))) | |||
Check if axis is exceed the game board boundary. | (defn out-of-boundary?
[[x y]]
(let [max-x (/ (:canvas/width @state) (:snake/width @state))
max-y (/ (:canvas/height @state) (:snake/height @state))]
(or (>= y max-y) (< y 0) (>= x max-x) (< x 0)))) | |||
Check if axis is collission with snake's body. | (defn self-collission?
[[x y]]
(let [{:keys [:snake/body]} @state]
(some #(axis-equal? [x y] %) body))) | |||
Check if asix if equal the food's axis. | (defn eat-food?
[[x y]]
(let [{:keys [:snake/food]} @state]
(axis-equal? [x y] food))) | |||
Generate the food on random coordinate. | (defn generate-food
[]
(let [{:keys [:snake/body :snake/food :snake/food-color]} @state
max-x (/ (:canvas/width @state) (:snake/width @state))
max-y (/ (:canvas/height @state) (:snake/height @state))]
;; skip when current food exist
(when (nil? food)
;; generate food axis
(loop [food [(rand-int max-x) (rand-int max-y)]]
(if-not (self-collission? food)
(do
(swap! state assoc-in [:snake/food] food)
(draw food food-color)
food)
(recur [(rand-int max-x) (rand-int max-y)])))))) | |||
The main game-loop. | (defn game-loop
[]
(let [{:keys [:canvas/background-color :snake/body :snake/body-color :snake/direction]} @state
head (axis-add (nth body 0) direction)
tail (last body)]
;; Every time enter game-loop, check if we need to generate new food or not
(generate-food)
;; Detect if snake
(when (self-collission? head)
(js/alert (str "Snake is collission with itself at : " head))
(swap! state assoc-in [:snake/alive] false))
;; Detect if snake excessed the boundary
(when (out-of-boundary? head)
(js/alert (str "Snake is out of boundary at :" head))
(swap! state assoc-in [:snake/alive] false))
;; When snake is alive, draw the snake and switch next game-loop
(when (:snake/alive @state)
;; Draw head
(draw head body-color)
;; Detect if food eat by snake or not
(if (eat-food? head)
;; When food was eaten by snake, just increase it's head and not remove tail
(swap! state assoc
:snake/food nil
:snake/body (conj body head))
(do
;; Remove tail by draw canvas's background-color
(draw tail background-color)
;; Add head and remove tail
(swap! state assoc-in [:snake/body] (-> (conj body head) drop-last))))
;; next move, you can modify the `150` to change different speed
(js/window.setTimeout (fn [] (game-loop)) 150)))) | |||
(defn init [] ;; Resize canvas object (resize-canvas) ;; Remove all listen events (events/removeAll js/document) ;; Register event listener `on-keydown` event (events/listen js/document goog.events.EventType.KEYDOWN on-keydown) ;; Start the game loop (game-loop)) | ||||