夢とガラクタの集積場

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

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

こんにちは。

今まで確認してきた状態管理のAPIを使い、「SnakeGame」を作ってみます。
と言いつつ、そもそもSnakeGameを起動するところからしてはまったのでそこから(汗

まず、前提として「https://github.com/stuarthalloway/programming-clojure」からダウンロードしたソースが
Replを実行しているディレクトリ配下に展開し、Cljファイル群が「src」ディレクトリ配下に配置されているとします。
階層としてはこんな感じ。

root
├─src
│  ├─examples
│  │  └─atom_snake.clj etc
│  │
│  └─reader
│      └─TaskList.clj etc
・・・

この前提で進めます。

user=> (load-file "src/examples/import_static.clj")
#'examples.import-static/import-static
user=> (load-file "src/examples/snake.clj")
#'examples.snake/game
user=> (use 'examples.snake)
nil
user=> (game)
[#<Ref@130362d0: {:body ([1 1]), :dir [1 0], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}> #<Ref@3004ed34: {:location [27 2], :color #<Color java.awt.Color[r=210,g=50,b=90]>, :type :apple}> #<Timer javax.swing.Timer@c361f9c>]

上記のコードを実行すると画面が起動し、以下の動作をするゲームが動作します。

  • 矢印でヘビアイコン(緑色)を動かす
  • リンゴアイコン(赤色)をヘビアイコンで食べるとヘビの身体の長さが伸びる
  • 3回食べると終了

実際の画面イメージはこんな感じ。非常にシンプルです。

このゲームを関数型言語っぽく以下のように分割して作成していく形になります。

  1. 関数モデル層:純粋関数として切り出したモデル
  2. 変更可能モデル層:ゲーム内の変化する状態を扱う層
  3. GUI

Clojure-Bookに従い、以下のファイルに実際のコードを記述していきます。
まずは「関数モデル層」からです。

user=> (load-file "src/reader/snake.clj")
#'reader.snake/dirs

■snake.clj(1):定数+汎用的な加算関数

; Game Window Width
(def width 75)
; Game Window Height
(def height 50)
; Snake and Target Size
(def point-size 10)
; Display Update Interval
(def turn-millis 75)
; Snake Finish Length
(def win-length 5)
; Snake Direction Move Value
(def dirs { VK_LEFT [-1 0] VK_RIGHT [ 1 0] VK_UP [ 0 -1] VK_DOWN [ 0 1]})

; add Several Points
(defn add-points [& pts]
  (vec (apply map + pts)))

; Convert Point to Rectangles
(defn point-to-screen-rect [pt]
  (map #(* point-size %)
  [(pt 0) (pt 1) 1 1]))

■snake.clj(2):リンゴアイコン+ヘビアイコンの初期化

; Create Apple
(defn create-apple []
    {:location [(rand-int width) (rand-int height)] :color (new Color 210 50 90) :type :apple})

; Create Snake
(defn create-snake []
    {:body (list [1 1]) :dir [1 0] :type :snake :color (new Color 15 160 70)})

ここまで来ると、以下のコマンドを入力することで初期化関数の動作を確認することが可能です。

user=> (load-file "src/examples/import_static.clj")
#'examples.import-static/import-static
user=> (load-file "src/reader/snake.clj")
#'reader.snake/move-snake
user=> (use 'reader.snake)
nil
user=> (create-snake)
{:body ([1 1]), :dir [1 0], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}
user=> (create-apple)
{:location [3 6], :color #<Color java.awt.Color[r=210,g=50,b=90]>, :type :apple}
user=> (create-apple)
{:location [6 5], :color #<Color java.awt.Color[r=210,g=50,b=90]>, :type :apple}
user=> (create-apple)
{:location [58 45], :color #<Color java.awt.Color[r=210,g=50,b=90]>, :type :apple}

■snake.clj(3):ヘビアイコンの移動

; Move Snake
(defn move-snake [{:keys [body dir] :as snake} & grow]
    (assoc snake :body (cons (add-points (first body) dir)
        (if grow body (butlast body)))))
; ヘビのBodyを入れ替えることでヘビの前進を表現している。growフラグ未設定の場合はbodyの最後を移動後に削除
user=> (move-snake (create-snake))
{:body ([2 1]), :dir [1 0], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}
; 成長フラグ未設定
user=> (move-snake (create-snake) :grow)
{:body ([2 1] [1 1]), :dir [1 0], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}
; 成長フラグ設定

■snake.clj(4):勝利/敗北/リンゴの判定

; Judge Win
; 身体の長さが5以上になったら勝利と判定
(defn game-win? [{body :body}]
    (>= (count body) win-length))
; Judge overlap=lose
; 身体に頭と同じ座標が含まれていたら敗北と判定
(defn head-overlaps-body? [{body :body}]
    (contains? (set (rest body)) (first body)))
(def lose? head-overlaps-body?)
; Judge eats?
; 頭の座標(bodyの1要素目)とlocationが一致していたら食べると判定
(defn eats? [{[snake-head] :body} {apple :location}]
    (= snake-head apple))
user=> (game-win? {:body [1 2 3 4 5]})
true
user=> (game-win? {:body [1 2 3 4]})
false
user=> (lose? {:body [[1 1] [1 1] [1 3]]})
true
user=> (lose? {:body [[1 1] [1 2] [1 3]]})
false
user=> (eats? {:body [[1 1] [1 2] [1 3]]} {:location [1 1]})
true
user=> (eats? {:body [[1 1] [1 2] [1 3]]} {:location [1 2]})
false

■snake.clj(5):ヘビの向きを変える

; Turn Snake
(defn turn-snake [snake newdir]
    (assoc snake :dir newdir))
; ヘビのdirを入れ替えることでヘビの方向変更を表現している。
user=> (turn-snake
 (create-snake) [1 1])
{:body ([1 1]), :dir [1 1], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}
user=> (turn-snake
 (create-snake) [1 2])
{:body ([1 1]), :dir [1 2], :type :snake, :color #<Color java.awt.Color[r=15,g=160,b=70]>}

と、入力だけで出力が決まり、かつGUIが絡まないのはこの位になりそうです。
現状まだヘビが動くしかないわけですが、この段階でも確認ができるのは大きいですね。

では、次は状態管理部分になります。