SnakeゲームでClojure入門
下記書籍の例題「Snakeゲーム」で Clojure に入門したのでメモ。
- 作者: Stuart Halloway and Aaron Bedra,川合史朗
- 出版社/メーカー: オーム社
- 発売日: 2013/04/26
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (11件) を見る
サンプルコード取得元
実装
初期状態(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