basic-snake


A basic snake game only use traditional html method. (Without react.js, vue.js ...etc)

dependencies

org.clojure/clojurescript
1.9.562



(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))