SnakeゲームでClojure入門 その2
「SnakeゲームでClojure入門 - oknknicの日記」のSnakeゲームを少し改良してみたので以下に貼っておく。
; Snakeゲーム(改良版) ; 【改良点】 ; 1)機能追加、キー割当追加 ; s: ゲーム開始/一時停止/一時停止解除 ; r: ゲームリセット ; n: ゲーム一時停止中に、1時間単位進める ; 2)壁の導入 (ns reader.snake (:import (java.awt Color Dimension) (javax.swing JPanel JFrame Timer JOptionPane) (java.awt.event ActionListener KeyListener KeyEvent))) ; ---------------------------------------------------------- ; 関数モデル(functional model)層 ; ---------------------------------------------------------- (def width 30) ; 有効範囲は 0 〜 width-1 (def height 30) ; 有効範囲は 0 〜 height-1 (def point-size 10) (def turn-millis 75) ; heart-beat (def win-length 30) (def dirs {KeyEvent/VK_LEFT [-1 0] ; Event#getKeyCodeの戻り値に応じた方向を定義 KeyEvent/VK_RIGHT [ 1 0] KeyEvent/VK_UP [ 0 -1] KeyEvent/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 create-wall "ゲーム領域境界の壁を算出します。 useage: (create-wall)" [] {:body (for [x (range -1 (inc width)) y (range -1 (inc height)) :when (or (= x -1) (= x width) (= y -1) (= y height))] [x y]) ; リスト内包表記 :type :wall :color (Color. 153 76 0)}) (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 head-overlaps-wall? "ヘビと壁から、ヘビの頭が壁に被っているかを算出します。 useage: (head-overlaps-wall? {:body [[1 0] [1 1]]} {:body [[0 0] [0 1] [0 2] [1 0] [1 2] [2 0] [2 1] [2 2]]}) => true" [{[snake-head] :body} {wall :body}] (contains? (set wall) snake-head)) (defn lose? "ヘビと壁から、ゲームに敗北済であるかを算出します。 useage: (lose? {:body [[3 1] [3 1] [2 1] [1 1]]} {:body [[0 0] [0 1] [0 2] [1 0] [1 2] [2 0] [2 1] [2 2]]}) => true" [snake wall] (or (head-overlaps-body? snake) (head-overlaps-wall? snake wall))) (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 wall] (dosync (ref-set apple (create-apple)) (ref-set snake (create-snake)) (ref-set wall (create-wall))) 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))) (defmethod paint :wall [g {:keys [body color]}] (doseq [point body] (fill-point g point color))) (defn toggle-timer "タイマーの開始/終了状態をトグルします。" [timer] (if (. timer isRunning) (. timer stop) (. timer start))) (defn action-once "1度だけアクションを起こします。" [timer] (dorun (map #(. % actionPerformed nil) (. timer getActionListeners)))) ; dorunにより、遅延シーケンスを強制評価 (defmulti key-action "キー入力に応じて動作します。" (fn [key-code & _] (if ((complement nil?) (dirs key-code)) :dirs key-code))) (defmethod key-action :dirs ;ヘビの方向を変更します。 [key-code snake & _] (update-direction snake (dirs key-code))) (defmethod key-action KeyEvent/VK_S ;ゲームの一時停止/再開をトグルします。 [_ _ _ _ _ _ timer & _] (toggle-timer timer)) (defmethod key-action KeyEvent/VK_R ;ゲームをリセットします。 [_ snake apple wall _ panel timer & _] (reset-game snake apple wall) (. timer stop) (. panel repaint)) (defmethod key-action KeyEvent/VK_N ;1時間単位進めます。 [_ _ _ _ _ _ timer & _] (action-once timer)) (defmethod key-action :default [& _] ;動作未定義のため、何もしません。 nil) (defn game-panel "ゲーム画面用のパネルを生成します。" [snake apple wall] (proxy [JPanel] [] (paintComponent [g] ; パネル描画の際に呼びされる関数 (proxy-super paintComponent g) (paint g @snake) (paint g @apple) (paint g @wall)) (getPreferredSize [] ; パネルの初期サイズを決定する関数 (Dimension. (* width point-size) (* height point-size))))) (defn game-listener "ゲーム画面用のリスナーを生成します。" [snake apple wall frame panel timer] (proxy [ActionListener KeyListener] [] (actionPerformed [e] ; タイマー割り込み毎に呼び出される関数 (update-positions snake apple) (when (lose? @snake @wall) (reset-game snake apple wall) (JOptionPane/showMessageDialog frame "You lose!") (. timer stop)) (when (win? @snake) (reset-game snake apple wall) (JOptionPane/showMessageDialog frame "You win!") (. timer stop)) (.repaint panel)) (keyPressed [e] (key-action (.getKeyCode e) snake apple wall frame panel timer)) (keyReleased [e]) ; 何もしない (keyTyped [e]))) ; 何もしない (defn game "Snakeゲームを開始します。" [] (let [snake (ref (create-snake)) apple (ref (create-apple)) wall (ref (create-wall)) frame (JFrame. "Snake") panel (game-panel snake apple wall) timer (Timer. turn-millis nil) ; timerとlistenerは相互参照関係のため、timerへのlistenerの設定は別途実施 listener (game-listener snake apple wall frame panel timer)] (doto panel (.setFocusable true) (.addKeyListener listener)) (doto frame (.add panel) (.pack) (.setVisible true)) (. timer addActionListener listener) [snake, apple, timer]))