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