SnakeゲームでClojure入門

下記書籍の例題「Snakeゲーム」で Clojure に入門したのでメモ。

プログラミングClojure 第2版

プログラミングClojure 第2版

ちなみに環境は以下の通り。

サンプルコード取得元

アーキテクチャ

  • 関数モデル(functional model)層(純粋関数)
  • 変更可能モデル(mutable model)層
  • GUI層(Swing)

実装

初期状態(reader/snake.clj)
; START: namespace
(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)
; END: namespace

; TODO: implement the Snake!
実装

※ソースファイルはUTF-8(BOMなし)で保存すること!

; Snakeゲーム
(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)

; ----------------------------------------------------------
; 関数モデル(functional model)層
; ----------------------------------------------------------
(def width 30)
(def height 30)
(def point-size 10)
(def turn-millis 75) ; heart-beat
(def win-length 30)
(def dirs {VK_LEFT  [-1  0] ; Event#getKeyCodeの戻り値に応じた方向を定義
           VK_RIGHT [ 1  0]
           VK_UP    [ 0 -1]
           VK_DOWN  [ 0  1]})

(defn add-points
	"移動前の座標と移動量から、移動後の座標を算出します。
	useage: (add-points [3 5] [1 1])
	=> [4 6]"

	[& pts]
	(vec (apply map + pts)))

(defn point-to-screen-rect
	"座標から、ビットマップ上の正方形表現(x,y,横幅,縦幅からなるベクタ)を算出します。
	useage: (point-to-screen-rect [3 5])
		=> (30 50 10 10) ※point-size が 10 の場合の例"

	[pt]
	(map #(* point-size %)
	     [(pt 0) (pt 1) 1 1]))

(defn create-apple
	"新規のリンゴ(座標,色,タイプからなるマップ)を算出します。
	useage: (create-apple)
		=> {:location [73 44], :color #<Color java.awt.Color[r=210,g=50,b=90]>, :type :apple} ※locationの値は乱数"

	[]
	{:location [(rand-int width) (rand-int height)]
	 :color (Color. 210 50 90)
	 :type :apple})

(defn create-snake
	"新規のヘビ(座標リスト,方向,色,タイプからなるマップ)を算出します。
	useage: (create-snake)
		=> {:body ([1 1]), :dir [1 0], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}"

	[]
	{:body (list [1 1])
	 :dir [1 0]
	 :type :snake
	 :color (Color. 15 160 70)})

(defn move
	"移動前のヘビから移動後のヘビを算出します。
	useage: (move (create-snake))
		=> {:body ([2 1]), :dir [1 0], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}"

	[{:keys [body dir] :as snake} & grow]		; 分配束縛。body, dir には引数のそれぞれのキー値を、snakeには引数本体を束縛
	(assoc snake :body				; ヘビの体を置き換えたシーケンスを生成
		(cons (add-points (first body) dir)	; ヘビの体の先頭に、移動後の座標を追加する
		      (if grow body (butlast body)))))	; 引数にgrow(成長フラグ)が渡されなかった場合は、ヘビの体の末尾を除去(移動であるため)

(defn win?
	"ヘビから、ゲームに勝利済であるかを算出します。
	useage: (win? {:body [[1 1] [1 2]]})
		=> false  ※win-lengthが3以上の場合の例"

	[{body :body}]
	(>= (count body) win-length))

(defn head-overlaps-body?
	"ヘビから、頭が体に被っているかを算出します。
	useage: (head-overlaps-body? {:body [[3 1] [3 1] [2 1] [1 1]]})
		=> true"

	[{[head & body] :body}]
	(contains? (set body) head))

(defn lose?
	"ヘビから、ゲームに敗北済であるかを算出します。
	useage: (lose? {:body [[3 1] [3 1] [2 1] [1 1]]})
		=> true"

	[snake]
	(head-overlaps-body? snake))

(defn eats?
	"ヘビとリンゴから、ヘビがリンゴを食べられるか(ヘビの頭の座標にリンゴが存在するか)を算出します。
	useage: (eats? {:body [[1 1] [2 1]]} {:location [1 1]})
		=> true"

	[{[snake-head] :body} {apple :location}]	; 分配束縛。snake-headはヘビの体の1番目の座標
	(= snake-head apple))

(defn turn
	"方向変更前のヘビと新しい方向から、方向変更後のヘビを算出します。
	useage: (turn (create-snake) [0 -1])
		=> {:body ([1 1]), :dir [0 -1], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}"

	[snake newdir]
	(assoc snake :dir newdir))


; ----------------------------------------------------------
; 変更可能モデル(mutable model)層
; ----------------------------------------------------------
(defn reset-game
	"ゲームを初期状態に戻します(引数のリファレンス(ヘビとリンゴ)を初期化します)。
	useage:	(#(let [snake (ref nil) apple (ref nil)] (reset-game snake apple) {:snake @snake :apple @apple}))
		=> {:snake {:body ([1 1]), :dir [1 0], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}, :apple {:location [20 23], :color #<Color java.awt.Color[r=210,g=50,b=90]>, :type :apple}}"

	[snake apple]
	(dosync (ref-set apple (create-apple))
	        (ref-set snake (create-snake)))
	nil)

(defn update-direction
	"ヘビの方向を変更します。
	useage: (#(let [snake (ref nil)] (dosync (ref-set snake (create-snake)) (update-direction snake [0 -1])) @snake))
		=> {:body ([1 1]), :dir [0 -1], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}"

	[snake newdir]
	(when ((complement nil?) newdir) 		 ; 方向未指定の場合は何もしない
	      (dosync (alter snake turn newdir))))

(defn update-positions
	"1ターン進めます。
	useage: (#(let [snake (ref nil) apple (ref nil)] (dosync (ref-set snake (create-snake)) (update-positions snake apple)) {:snake @snake :apple @apple}))
		=> {:snake {:body ([2 1]), :dir [1 0], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}, :apple nil}"

	[snake apple]
	(dosync
		(if (eats? @snake @apple)
			(do (ref-set apple (create-apple))
			    (alter snake move :grow))
			(alter snake move)))
	nil)


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

(defmulti paint	(fn [g object & _] (:type object)))	; マルチメソッド。オブジェクトのタイプで呼び分ける
(defmethod paint :apple [g {:keys [location color]}]
	(fill-point g location color))
(defmethod paint :snake [g {:keys [body color]}]
	(doseq [point body]
		(fill-point g point color)))

(defn game-panel
	"ゲーム画面用のパネルを生成します(各種リスナーIFも実装)。"

	[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])))	; 何もしない

(defn game
	"Snakeゲームを開始します。"

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

読み込み・実行

(use 'reader.snake) ; :reloadオプション付与で強制読み込み
(game)
補足:定義内容確認
(dir reader.snake) ; Prints a sorted directory of public vars in a namespace
(source add-points) ; Prints the source code for the given symbol
(doc add-points) ; Prints documentation for a var or special form given its name