(ns reader.snake
(:import (java.awt Color Dimension)
(javax.swing JPanel JFrame Timer JOptionPane)
(java.awt.event ActionListener KeyListener))
(:use examples.import-static))
(import-static java.awt.event.KeyEvent VK_LEFT VK_RIGHT VK_UP VK_DOWN)
首先使用ns聲明了名稱空間,然後導入了需要的Java類和另一個名稱空間,最後使用了名稱空間的import-static宏導入一些靜态成員。接下來看看import-static宏是如何編寫的。
(defmacro import-static
[class & fields-and-methods]
(let [only (set (map str fields-and-methods))
the-class (. Class forName (str class))
static? (fn [x]
(. java.lang.reflect.Modifier (isStatic (. x (getModifiers)))))
statics (fn [array]
(set (map (memfn getName) (filter static? array))))
all-fields (statics (. the-class (getFields)))
all-methods (statics (. the-class (getMethods)))
fields-to-do (intersection all-fields only)
methods-to-do (intersection all-methods only)
make-sym (fn [string]
(with-meta (symbol string) {:private true}))
import-field (fn [name]
(list 'def (make-sym name) (list '. class (symbol name))))
import-method (fn [name]
(list 'defmacro (make-sym name) '[& args] (list 'list ''. (list 'quote class) (list 'apply 'list (list 'quote (symbol name)) 'args))))]
`(do [email protected](map import-field fields-to-do)
[email protected](map import-method methods-to-do))))
為了閱讀友善,删除了檔案頭部的名稱空間聲明和宏文檔字元串。
首先看參數清單,class為傳入的Java類(如上文的java.awt.event.KeyEvent),fields-and-methods為要導入的靜态域和方法名。
接下來是一個很長的let,用來綁定了很多的局部名稱。
- only是一個傳入的靜态域和靜态方法的集合
- the-class是傳入類的Class對象,用于反射
- static?是一個函數,用于判定傳入的類成員是否為靜态成員。
- statics是一個函數,接收一個類成員的數組,傳回所有的靜态成員名。
- all-fields代表目前類的所有靜态域
- all-methods代表目前類的所有靜态方法。
- fields-to-do要導入的靜态域
- methods-to-do要導入的靜态方法
- make-sym是一個函數,根據傳入的字元串建立一個symbol
- import-field是一個函數,使用def建立靜态域對應的symbol
- import-method是一個函數,使用defmacro建立靜态域對應的宏(要導入的靜态方法被定義為宏)。
The Functional Model
(def width)
(def height)
(def point-size)
(def turn-millis)
(def win-length)
(def dirs { VK_LEFT [-1]
VK_RIGHT []
VK_UP [ -1]
VK_DOWN []})
width表示視窗寬有75個正方形,height表示視窗高有50個正方形,point-size表示每個正方形的邊長(像素)。
turn-millis表示多長時間重新整理一次遊戲視窗(重繪),win-length表示赢得遊戲時snake的長度。dirs表示方向向量。
;用于兩個坐标向量相加,傳回結果向量
(defn add-points [& pts]
(vec (apply map + pts)))
;根據點的坐标向量計算該正方形的繪制資訊(包括左上角的坐标和寬高,以像素為機關)
(defn point-to-screen-rect [pt]
(map #(* point-size %)
[(pt) (pt)]))
;傳回代表蘋果的map
(defn create-apple []
{:location [(rand-int width) (rand-int height)]
:color (Color.)
:type :apple})
;傳回代表蛇的map
(defn create-snake []
{:body (list [1])
:dir [1]
:type :snake
:color (Color.)})
;傳入蛇的map,首先蛇頭和方向向量相加,然後判斷grow參數,grow參數存在就加上原來全部的蛇身,否則就加上舍棄尾部的蛇身。
;注意函數中對傳入map的拆解并綁定相應的字段,:as後面的符号綁定整個map
(defn move [{:keys [body dir] :as snake} & grow]
(assoc snake :body (cons (add-points (first body) dir)
(if grow body (butlast body)))))
;更改蛇map的:dir字段
(defn turn [snake newdir]
(assoc snake :dir newdir))
;判斷是否赢得了遊戲
(defn win? [{body :body}]
(>= (count body) win-length))
;判斷除頭部的身體集合是否包含了蛇頭。
(defn head-overlaps-body? [{[head & body] :body}]
(contains? (set body) head))
;判斷遊戲是否失敗
(def lose? head-overlaps-body?)
;判斷頭部和蘋果的坐标向量是否相等(是否吃掉了蘋果)
(defn eats? [{[snake-head] :body} {apple :location}]
(= snake-head apple))
Building a Mutable Model with STM
注意下面函數的參數都是ref,因而必要的時候使用了解引用reader macro(@)跟上面的函數進行銜接。
;每次timer回調都會調用該函數,更改蛇的狀态,要麼吃掉蘋果增長一節,要麼正常移動。
(defn update-positions [snake apple]
(dosync
(if (eats? @snake @apple)
(do (ref-set apple (create-apple))
(alter snake move :grow))
(alter snake move)))
nil)
;更改snake的方向向量
(defn update-direction [snake newdir]
(when newdir (dosync (alter snake turn newdir))))
;重置遊戲,即建立新的蛇和蘋果
(defn reset-game [snake apple]
(dosync (ref-set apple (create-apple))
(ref-set snake (create-snake)))
nil)
The Snake GUI
;該函數繪制一個方塊
(defn fill-point [g pt color]
(let [[x y width height] (point-to-screen-rect pt)]
(.setColor g color)
(.fillRect g x y width height)))
;該處多重方法,分别用來繪制蘋果和蛇,注意此處使用了匿名方法,根據第二個參數的:type字段進行排程
(defmulti paint (fn [g object & _] (:type object)))
(defmethod paint :apple [g {:keys [location color]}]
(fill-point g location color))
;doseq作用為,将body中的每個點向量綁定到point上,并進行繪制
(defmethod paint :snake [g {:keys [body color]}]
(doseq [point body]
(fill-point g point color)))
;函數傳回一個實作了ActionListener和KeyListener接口的JPanel對象
;proxy用于繼承一個類并且實作接口,在這兒是繼承了JPanel,然後實作了後面的接口。
(defn game-panel [frame snake apple]
(proxy [JPanel ActionListener KeyListener] []
(paintComponent [g]
(proxy-super paintComponent g)
(paint g @snake)
(paint g @apple))
(actionPerformed [e]
(update-positions snake apple)
(when (lose? @snake)
(reset-game snake apple)
(JOptionPane/showMessageDialog frame "You lose!"))
(when (win? @snake)
(reset-game snake apple)
(JOptionPane/showMessageDialog frame "You win!"))
(.repaint this))
(keyPressed [e]
(update-direction snake (dirs (.getKeyCode e))))
(getPreferredSize []
(Dimension. (* (inc width) point-size)
(* (inc height) point-size)))
(keyReleased [e])
(keyTyped [e])))
;遊戲入口函數,建立snake,apple,frame,panel,timer
(defn game []
(let [snake (ref (create-snake))
apple (ref (create-apple))
frame (JFrame. "Snake")
panel (game-panel frame snake apple)
timer (Timer. turn-millis panel)]
(doto panel
(.setFocusable true)
(.addKeyListener panel))
(doto frame
(.add panel)
(.pack)
(.setVisible true))
(.start timer)
[snake, apple, timer]))