夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

Clojure勉強日記(その24 SnakeGameをClojureで書いてみる(その3

こんにちは。いよいよ大詰め。
今まではコマンドライン上でしか動作しなかったゲームをGUI上で表示してみます。

■snake.clj:Graphicの塗りつぶし処理

; Fill-Points
(defn fill-point [graphic pt color]
   (let [[x y width heignt] (point-to-screen-rect pt)]
     (.setColor graphic color)
     (.fillRect graphic x y width heignt)))

; Paint Multi Method
(defmulti paint (fn [graphic object & _] (:type object)))

(defmethod paint :apple [graphic {:keys [location color]}]
  (fill-point graphic location color))

(defmethod paint :snake [graphic {:keys [body color]}]
  (doseq [point body]
    (fill-point graphic point color)))

defmultiは引数の型によって処理を分ける「マルチメソッド」を定義するものです。
Javaでいうオーバーロードにあてはまるようですが、動的型付けのClojureでも定義は可能なようです。
上記のメソッド群でappleとsnake用に四角を描画する処理が記述されています。
■snake.clj:パネル生成

; Create Game Panel
(defn game-panel [frame snake apple]
  (proxy [JPanel ActionListener KeyListener] []
    (paintComponent [graphic]
      (proxy-super paintComponent graphic)
      (paint graphic @snake)
      (paint graphic @apple))
    (actionPerformed [event]
      (update-position snake apple)
      (when (lose? @snake) (initialize-game snake apple) (JOptionPane/showMessageDialog frame "You lose!"))
      (when (game-win? @snake) (initialize-game snake apple) (JOptionPane/showMessageDialog frame "You win!"))
      (.repaint this))
    (keyPressed [event]
      (update-direction snake (dirs (.getKeyCode event))))
    (getPreferredSize []
      (new Dimension (* (inc width) point-size) (* (inc height) point-size)))
    (keyReleased [event])
    (keyTyped [event])))

若干長く見えるかもしれませんが、実際はJPanel ActionListener KeyListenerを継承して
メソッドを数行で書いているのみです。
内容自体はコンポーネントを記述して、イベントごとに方向と場所を更新して勝利/敗北判定を続ける・・・
という非常に分かりやすい内容になっています。

■snake.clj:初期化

; Start Game
(defn game []
  (let [snake (ref (create-snake)) apple (ref (create-apple)) frame (new JFrame "Snake Game") panel (game-panel frame snake apple) timer (new Timer turn-millis panel)]
    (doto panel (.setFocusable true) (.addKeyListener panel))
    (doto frame (.add panel) (.pack) (.setVisible true))
    (.start timer)
    [snake, apple, timer]))

これで初期化も完了です。
ちなみに、最後の返り値として[snake, apple, timer]を返す必要は実際には無いのですが、
その方がREPL側から確認しやすいため返り値を返しています。

■snake.clj:実行
というわけで、下記のコマンドを入力して実行してみますと・・・

user=> (load-file "src/examples/import_static.clj")
#'examples.import-static/import-static
user=> (load-file "src/reader/snake.clj")
#'reader.snake/game
user=> (load 'reader.snake)
ClassCastException clojure.lang.Symbol cannot be cast to java.lang.String  clojure.core/load (core.clj:5521)
user=> (use 'reader.snake)
nil
user=> (game)
[#<Ref@19a7b6e3: {:body ([1 1]), :dir [1 0], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}> #<Ref@3f58cda2: {:location [27 5], :color #<Color java.awt.Color[r=210,g=50,b=90]>, :type :apple}> #<Timer javax.swing.Timer@3c1cd99>]

以下のようにゲームの画面を表示することができました。
SwingのゲームなんてJavaで作ると小規模なものであってもやたらとソースが多くなりますが、
Clojureだと意外に少なく作ることができるんですね。