Arantium Maestum

プログラミング、囲碁、読書の話題

Reactで書かれた囲碁ボードをReagentで作ってみる(Core編)

前々回前回からの続き。

あとは作ったViewsとLogicを組み合わせるだけなので非常に簡単。

まずは状態を管理するreagent atomをdefonceしておく。

(defonce game-state (r/atom (l/new-game-state 9)))

defではなくdefonceなのは、defだとコードに変更を加える度にfigwheelで表示されているページの状態がリセットされてしまうため。

次は点にクリックされた時にこのratomをlogicに記述されているplay-moveで更新する関数:

(defn click [coord]
  (swap! game-state #(l/play-move % coord)))

パスボタンがクリックされた時の関数:

(defn pass []
  (swap! game-state l/pass))

viewsに定義されているGameコンポーネントにratomの状態(あくまでimmutableなデータ)と上記の定義された関数二つを渡す:

(defn app []
  [v/Game @game-state click pass])

考えてみたらviewsは本当に渡されたimmutableなデータを元にimmutableなデータ構造を返すだけだ。reagentをインポートすらしていないし、ratomに対して何かをするというロジックも組み込まれていない。

そういうデザインができるのはアプリが非常に単純だからでもあるけど、こういう風に綺麗にコードの分割が実現できると気持ちが良い。clojurescriptとreagentを使っている醍醐味の一つだろう。

あとはreagentでレンダするだけ:

(r/render
  [app]
  (js/document.getElementById "container"))

さて、一旦チュートリアルをベースに作ってみたが、その経験を元に一から作り直してみたい。特にデータ構造の定義をlogicとは別のところでやって、clojure.specを使い倒してみようと思う。

Reactで書かれた囲碁ボードをReagentで作ってみる(Views編)

前回からの続き。

Reagentコンポーネントを定義するviews.cljsの説明。

まずは点の大きさを表すGRID_SIZEを定義し、盤上の「点」をコンポーネント化する。

(def GRID_SIZE 40)

(defn BoardIntersection [[row col] color click-fn]
  (let [style {:top (* row GRID_SIZE)
               :left (* col GRID_SIZE)}
        classes (str "intersection" 
                     (cond (= color :black) " black"
                           (= color :white) " white"
                           :else            ""))]
    [:div {:className classes 
           :style style 
           :onClick #(click-fn [row col])}]))

クリックされた時の挙動は完全に外から渡されたclick-fnに準拠する。ゲームのロジックは全く組み込まれていない。のでこのコンポーネントを全く変えずに使ってオセロも作れる。

一つ気がかりなのは、盤上の端を特に考慮していないこと。できれば端は見た目を変えて表現したい。一から作る時にはこれもやってみよう。

BoardIntersectionコンポーネントを合わせてBoardViewコンポーネントを作る:

(defn BoardView [game-state click-fn]
  (let [size (:size game-state)
        style {:width (* size GRID_SIZE)
               :height (* size GRID_SIZE)}
        main-div [:div {:id "board"
                        :style style}]
        coords (for [x (range size) y (range size)] [x y])
        get-color (fn [coord] (get-in game-state [:board coord]))
        intersections (for [coord coords]
                            ^{:key coord} [BoardIntersection 
                              coord (get-color coord) click-fn])]
    (into main-div intersections)))

game-stateから盤の大きさと、各点の状態を取得して、その情報を元にforループで全ての点のIntersectionコンポーネントを作成し、親Divのベクトルにintoで挿入している。ここら辺のコンポジションはReact/Reagentの面目躍如と言ったところか。

get-color関数をここでも定義してしまっているのは残念。game-stateの実装の知識がここで漏れ出てしまっている。修正するときはgame-state関連の関数として定義したものをインポートして使うべきか。

パス用のボタン:

(defn PassView [pass-fn]
  [:input {:id "pass-btn"
           :type "button"
           :value "Pass"
           :onClick pass-fn}])

クリックされた時の挙動は完全に外部からのpass-fn次第。

特殊な状態を表示するAlertView:

(defn AlertView [game-state]
  (let [text (cond (:game-over game-state)
                   "GAME OVER"
                   (:in-atari game-state)
                   "ATARI"
                   (:attempted-suicide game-state)
                   "SUICIDE!"
                   :else
                   "")]
    [:div {:id "alerts"} text]))

React版をベースにしているのだけど、インターフェイスとしては少しいじりたくなる気がする。例えばアタリと自殺手が重複してたりすると自殺手のワーニングがでないのを、ゲームロジックの方で重複しないようにしている点。ここも要変更。

上記のコンポーネントを組み合わせてGameコンポーネントができる:

(defn Game [game-state click-fn pass-fn]
  [:div
   [AlertView game-state]
   [PassView pass-fn]
   [BoardView game-state click-fn]])

ゲーム状態やクリック時の挙動は外部から渡されるだけ。というわけでviews.cljsで定義されるコンポーネントはあくまで渡されたデータを元に見た目を構築するコードのみを記述したものになる。

views.cljsとlogic.cljsをcore.cljsで組み合わせて出来上がり。

core.cljsについては次回。

Reactで書かれた囲碁ボードをReagentで作ってみる(Logic編)

こういう記事を発見した。

React beginner tutorial: implementing the board game Go | Chris LaRose, Software Developer

JSとReactで簡単な囲碁のボードを作成する、というもの。コードとしても非常に簡単だったのでClojurescriptとReagentで書いてみた。 機能や大まかなロジックはReact版にある程度忠実に作ってある。が、Clojurescriptなのでほぼすべてが副作用なしの純粋関数。

囲碁のロジックをlogic.cljsファイルに書いてみる。

まずはデータ構造の定義:

(defn new-board [size]
  (zipmap
    (for [i (range size)
          j (range size)]
      [i j])
    (repeat :empty)))

(defn new-game-state [size]
  {:current-color :black
   :size size 
   :board (new-board size)
   :last-move-passed false
   :in-atari false
   :attempted-suicide false
   :game-over false})

盤上をベクトルのベクトルで表現してもいいのだが、各点を[i j]というベクトルのキーを使って表したマップが一番簡単だと思う。これがboardデータ。

ゲーム関連で直接盤上に関係しない、あるいは盤上から算出するよりフラグとして管理してしまった方がいい情報も加えてgame-stateデータを定義する。

後ほどよりClojureっぽく一から書き直してみたいと考えているのだが、その場合はここはclojure.specでちゃんと定義しよう。

次はパスに関する状態変化を定義:

(defn switch-player [game-state]
  (update game-state 
         :current-color
         #(if (= :black %) :white :black)))

(defn pass [game-state]
  (-> game-state
      (#(if (:last-move-passed %) 
          (assoc % :game-over true)
          %))
      (assoc :last-move-passed true)
      switch-player))

二回連続パスでゲーム終了なのだが、元のReactチュートリアルだとconsoleにゲームオーバーと出力するだけ。唐突にここでUIっぽいコードが出現するのもなんなので、フラグの一つをセットするだけにとどめる。 前回パスがあったかを確認する:last-move-passedフラグをtrueにし、次のプレーヤーを変更した状態のgame-stateを返す。

(defn valid-coord [game-state coord]
  (and
    (every? #(> % -1) coord)
    (every? #(< % (:size game-state)) coord)))

(defn get-adjacent [game-state [i j]]
  (->> [[i (dec j)]
        [i (inc j)]
        [(dec i) j]
        [(inc i) j]]
       (filter #(valid-coord game-state %))))

ヘルパー関数。ある点の周りにある点を返す。囲碁のルール上周囲の点の数は2、3、4が可能。

ある点に置かれた石と繋がっているすべての同色の石のグループと、そのグループの駄目(空いた点)の数を返すget-group関数の定義:

(defn get-group [move game-state]
  (let [color (get-in game-state [:board move])]
    (if-not (= color :empty)
      (loop [visited #{}
             visited-list []
             queue [move]
             liberties 0]
        (cond 
          (empty? queue)
          {:liberties liberties
           :stones visited-list}

          (visited (first queue))
          (recur visited visited-list (rest queue) liberties)

          :else
          (let [current   (first queue)
                neighbors (get-adjacent game-state current)
                empties   (filter #(= :empty (get-in game-state [:board %]))
                                  neighbors)
                sames     (filter #(= color (get-in game-state [:board %]))
                                  neighbors)]
            (recur 
              (conj visited current)
              (conj visited-list current)
              (into (rest queue) sames)
              (+ liberties (count empties)))))))))

挙動にあまり影響はないが、元のReactチュートリアルのコードの時点でバグがある。同じグループの二つの石の駄目が重複していると二重に数えてしまうので「当たり判定」がたまに間違っている。石を取る・取らないの段階ではどう数えても駄目は0なので、そこまで大きな問題ではないが・・・ 直そうと思うならlibertiesもsetにしてしまって最後に数えればいい。

盤上から石を取り除くヘルパー関数:

(defn remove-stone [game-state coord]
  (assoc-in game-state [:board coord] :empty))

(defn remove-stones [game-state coords]
  (reduce remove-stone game-state coords))

reduceが決まるとやっぱり気持ちいいな。

最後に現在のゲーム状態と次の一手の点を指定して、次のゲーム状態を返す関数play-moveを定義:

(defn play-move [game-state move]
  (if-not (= :empty (get-in game-state [:board move]))
    game-state
    (let [color (:current-color game-state)
          new-state (-> game-state
                        (assoc-in [:board move] color)
                        switch-player)
          neighbors (get-adjacent game-state move)
          n-others  (filter #(and (not= :empty (get-in new-state [:board %]))
                                  (not= color (get-in new-state [:board %]))) 
                            neighbors)
          n-groups  (map #(get-group % new-state) n-others)
          captured  (filter #(zero? (:liberties %)) n-groups)
          atari     (some #(= 1 (:liberties %)) n-groups)]
      (if (and (empty? captured)
               (-> move (get-group new-state) :liberties zero?))
       (-> game-state
         (assoc :attempted-suicide true))
       (-> new-state
         (remove-stones (->> captured (map :stones) (apply concat)))
         (assoc :in-atari atari)
         (assoc :attempted-suicide false)
         (assoc :last-move-passed false))))))

すでに石が打ってある点に打つのは無効。

打った点の周りにある敵の石とそれらの石が属するグループの駄目の状況をチェックし、もしそれらのグループの駄目が0になっていたら「取られている」と判定。

相手の石を取らず、逆に自分の石が取られてしまう(自分の石の駄目が0になる)手は無効。自殺手だというフラグをtrueにしてそれ以外は元の状態のまま返す。

そういう「着手禁止点」以外の場合、取られるべき石を取り除き、この手によってアタリになる相手の石があればアタリ判定フラグをtrueにし、自殺手とパスフラグをfalseにし、その手を盤上に置いて、次のプレーヤーの色を反転させたgame-stateを返す。

次はRegentでのUIだが長くなったので次回に。

コードはここ:

github.com

Clojure Web Development勉強 - Devcards(その1)最低限の設定

CSSをいじりたおす根気がなかった・・・

ということで話題を変えてFigwheelの作者でもあるBruce Haumanが作ったツールであるDevcardsについて書く。

Devcardsとは

DevcardsはClojureScript+Reactで作ったコンポーネントの動作を試すための環境を提供するツールである。

qiita.com

DevcardsをReagent+Figwheelと連動させる

今回はとりあえずDevcardsとFigwheelで、Reagentコンポーネントを表示・動的に変更できる環境を整える最低限の設定を紹介する。Devcardsはサンプルコードなどを見ると、SablonoというOmでよく使われるReact対応のHiccup構文ライブラリを使っていることが多いのだが、ちゃんとReagentにも対応しているので今までどおりReagentを使い続ける。

まずproject.clj:

(defproject min-devcards "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293"]
                 [reagent "0.6.0"]
                 [devcards "0.2.2"]]

  :plugins [[lein-cljsbuild "1.1.4"]
            [lein-figwheel "0.5.7"]]

  :cljsbuild
  {:builds {:devcards {:source-paths ["src"]
                       :figwheel {:devcards true}
                       :compiler {:output-to "resources/public/js/main.js"
                                  :output-dir "resources/public/js/out/"}}}})

:dependenciesにdevcardsを追加、そしてcljsbuildの:figwheelオプションを今までtrueだったのを{:devcards true}に変えてある。

今回は簡素化のために省いたが、cljsbuild内で定義される複数buildsのうち、本番環境用のコンパイル設定であるprod、開発環境用のdevに加えて、devcardsという三つ目のbuilds設定を作成することが推奨されている。

index.htmlは今までどおり:

<!DOCTYPE html>
<html>
    <body>
        <script src="js/out/goog/base.js"></script>
        <script src="js/main.js"></script>
        <script>goog.require('min_devcards.core')</script>
    </body>
</html>

次にcore.cljs:

(ns min-devcards.core
  (:require
    [reagent.core :as r])
  (:require-macros
    [devcards.core :as d :refer [defcard]]))

(defn my-app []
  [:div
   [:h1 "Hello Reagent!"]
   [:h2 "Hello Devcards!"]])

(defn text-renderer [text]
  [:div
   [:h1 "Here is your text:"]
   [:p text]])

(defcard myapp
  (d/reagent my-app))

(defcard text1
  (d/reagent [text-renderer "Hello"]))

(defcard text2
  (d/reagent [text-renderer "Goodbye"]))

とりあえずreagent.coreに加えてdevcards.coreからdefcardマクロを使う。reagentコンポーネントとして定義したものを、devcards.core/reagentというマクロでReactコンポーネント化している。それをdefcardsマクロが受けとって表示する流れとなる。

reagent.coreは実は今回はいらないが、今後r/atomを使う時のためにrequireしておく。

これでlein figwheelを実行するとこんな感じの表示になる:

f:id:zehnpaard:20161123165527p:plain

ちなみに複数のbuilds設定がproject.cljで定義されている場合はlein figwheel <build名>、例えばlein figwheel devcardsと実行することになる。

figwheelが走っているのでコンポーネントのコードや引数をいじって即時反映させるなどして、非常にインタラクティブなUI開発が可能になる。devcards固有の良さは、いろいろな状態の同一コンポーネントを並べて表示させることで、コード変更がどの状況でどのような影響があるかを非常に見やすくしてくれるところ。

次回はdevcardsのreagent atomとの関連を見ていく。

Clojure Web Development勉強 - Garden(その3)lein-gardenとfigwheelでgarden/CSS自動コンパイル&ロード

前々々回のfigwheelによるCSS自動ロードと前回のlein-gardenによるgarden→CSS自動コンパイルを合わせると、gardenデータ構造をcljファイルに記述・変更すると即時にブラウザで表示が更新されるような環境が出来上がる。

project.clj

leiningen及びlein-garden、figwheel、lein-cljsbuildへの指示は以下の通り:

(defproject figwheel-garden "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293"]
                 [reagent "0.6.0"]
                 [garden "1.3.2"]]

  :plugins [[lein-cljsbuild "1.1.4"]
            [lein-figwheel "0.5.7"]
            [lein-garden "0.3.0"]]

  :garden
  {:builds [{:source-paths ["src"]
             :stylesheet figwheel-garden.styles/style
             :compiler {:output-to "resources/public/css/main.css"}}]}

  :figwheel
  {:css-dirs ["resources/public/css"]}

  :cljsbuild
  {:builds {:dev {:source-paths ["src"]
                  :figwheel true
                  :compiler {:output-to "resources/public/js/main.js"
                             :output-dir "resources/public/js/out/"}}}})

今回はgardenはfigwheel-garden.stylesという名前空間に定義していく。こちらは.cljファイルなのでfigwheelが間違ってJavaScriptコンパイルするようなこともない。

figwheelとcljsbuildの設定は前々々回どおり。(cljsbuildのcompilerオプションがデフォルトで:optimizations :noneにしてくれるので今回は横着して明示的に指示していない)

Reagentによるコンテンツの定義

core.cljsでいつもどおり文書のコンテンツを定義していく:

(ns figwheel-garden.core
  (:require
    [reagent.core :as r]))

(defn my-app []
  [:div
   [:h1 "Hello Reagent!"]
   [:p "Hello Garden!"]
   [:p.my-class "Hello My-Class!"]])

(r/render
  [my-app]
  (js/document.getElementById "app"))

とりあえず普通のHTMLタグと一緒に、my-classというclassのp要素も定義してある。

Styleの定義

GardenでHTML要素の見た目を定義:

(ns figwheel-garden.styles
  (:require
    [garden.def :refer [defstyles]]))

(defstyles style
  [:body {:background "#ddd"}]
  [:h1 {:color "#f00"}]
  [:p {:font "18px \"Century Gothic\", Futura, sans-serif"}]
  [:.my-class {:font-size "20px" :background "#ddf"}])

いつもながらセンスのないスタイル。

HTMLとディレクトり構造

index.htmlでCSSとJSを呼び出し。ReactがDOM要素を挿入するためのdiv#appも忘れずに作っておく。

<html>
    <head>
        <link href="css/main.css" rel="stylesheet" type="text/css">
    </head>
    <body>
        <div id="app"></div>
        <script src="js/out/goog/base.js"></script>
        <script src="js/main.js"></script>
        <script>goog.require('figwheel_garden.core')</script>
    </body>
</html>

上記4つのファイルをこんな感じのディレクトリ構造に配置する:

.
├── project.clj
├── resources
│   └── public
│       └── index.html
└── src
    └── figwheel_garden
        ├── core.cljs
        └── styles.clj

あとはlein garden autoとlein figwheelを、順番に別々のシェルウィンドウで実行し、ブラウザでlocalhost:3449を開けば環境設定完了。

これでcore.cljsとstyles.cljのどちらをいじっても、保存した時点で変更が自動的にブラウザに送られる。例えばmy-appのトップレベルの:divを:div.my-classに変えることで、my-app内の背景の色をすべて薄い青にしてみたり。簡単なことでもけっこう遊びがいがある。お試しあれ。

次回はこの設定を使ってgardenの構文などを探ってみる。

今回のコード:

gist.github.com

Clojure Web Development勉強 - Garden(その2)lein-gardenでCSSをオートコンパイル

前回紹介したワークフローだとCSSをednで定義できるのはいいが、二つほど問題があった。

  1. 変更の度にいちいちlein runでCSSコンパイルする必要がある
  2. -main関数をCSSコンパイルのために使っていた

lein-gardenはこの二つの問題を同時に解決する、gardenコンパイル用のlein pluginである。

project.clj

dependenciesにgarden、pluginsにlein-garden、そしてgardenのコンパイル設定を別途指定している:

(defproject min-lein-garden "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [garden "1.3.2"]]
  :plugins [[lein-garden "0.3.0"]]
  :garden
  {:builds [{:source-paths ["src"]
             :stylesheet min-lein-garden.core/style
             :compiler {:output-to "resources/public/css/main.css"}}]})

:gardenの指示は以前からちょくちょく出ている:cljsbuildのコンパイル設定とけっこう似ている。

の三点を指定。コンパイルしたいcssファイルごとに:buildsの中のvectorに設定mapを入れてやればいい。

core.clj

CSS自体の定義はcore.cljで:

(ns min-lein-garden.core
  (:require [garden.def :refer [defstyles]]))

(defstyles style
  [:body
   {:font-size "14px"
    :background "#f00"}])

前回のようにcssマクロでgardenデータを文字列に変形するのではなく、defstylesというマクロを使ってstyleという名前のgardenデータ構造を定義している。project.cljに渡していた:stylesheetのmin-lein-garden.core/styleという変数名とこのdefstyleで定義している変数名は一致する必要がある。

ディレクトリ構造と実行方法

ディレクトリ構造は非常に簡単で、以下の通り:

.
├── project.clj
└── src
    └── min_lein_garden
        └── core.clj

lein-gardenが自動で出力先のディレクトリなどを作成してくれる。

シェル・プロンプトでlein garden autoと叩くと

Compiling Garden...

Compiling "resources/public/css/main.css"...

Wrote: resources/public/css/main.css

Successful

というような表示が出て、プロセスが走り続ける。別のウィンドウからresources/public/css/main.cssを覗いてみると

body {
  font-size: 14px;
  background: #f00;
}

とちゃんとコンパイルされているのがわかる。

これでcore.cljのdefstylesを以下のようにいじって保存すると:

(defstyles style
  [:body
   {:font-size "16px"
    :background "#ff0"
    :color "#fff"}])

lein garden autoが走っているシェルでは自動的に以下のように表示されて:

Compiling "resources/public/css/main.css"... Wrote: resources/public/css/main.css Successful

main.cssはこのようになっている:

body {
  font-size: 16px;
  background: #ff0;
  color: #fff;
}

これで-main関数を使わず、gardenデータを定義しているファイルを保存するだけで自動的にcssが出力される設定が実現できた。

次回はこれをfigwheelと合わせることで、gardenを定義・変更することでブラウザ上で動的にスタイルが変わっていくのを確認できる環境を設定する。

Clojure Web Development勉強 - Garden(その1)最低限の設定でCSSをコンパイルしてみる

前回に続いてCSSの話。

Reagent/Hiccupを使ってHTML/DOMをedn形式で定義しているのだから、できればCSSも同じくednで定義したい!という末期症状な人間のために、gardenというライブラリが存在している。

github.com

Hiccupと同じように、Clojureのデータ構造であるVectorとMapでCSSを定義できる。最近は生のCSSで書くのではなく、より表現力の高いPreprocessorなどを使って記述するケースが増えているようだが、そのようなPreprocessorよりもさらに強力なClojureのデータ構造処理の機能や関数化・データ再利用がすべて使えるというのは一つの強みである。

個人的にはそういったまっとうな理由よりも「どうせだったら全部ednで書こう」という思いのほうがgarden導入の強い動機になっているが。

今回は最低限gardenを使うための設定について。

Project.clj

(defproject min-garden "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [garden "1.3.2"]]
  :main min-garden.core)

今回はあくまでgardenデータの定義とコンパイルについてのみなので、ClojureScriptやそれに関係するライブラリはまったく使わない。ClojureそのものとGardenをdependenciesに入れて、コンパイルさせるためにmain関数のありかを指定している。

core.clj

今回は.cljsではなく.cljファイルを定義:

(ns min-garden.core
  (:require [garden.core :refer [css]]))

(def style
  [:body {:font-size "16px"}])

(defn -main []
  (css {:output-to "resources/public/css/main.css"}
    style))

garden.coreからcssマクロをreferしている。styleというのはただのデータ構造で、vectorに:bodyキーワードとmapが入っていて、mapに:bodyに対して適用するスタイルの宣言が入っている。

-mainには、styleデータに基づいたcssをresources/public/css/main.cssというファイルにコンパイルする、という指示が入っている。ちなみにファイル指定なしで(css style)などとするとstyleデータをcssフォーマットに直した文字列が返される。

ディレクトリ構造とコンパイル指示

resources/public/cssというディレクトリが存在しないとエラーになるので、mkdirなどでディレクトリ構造をちゃんと整える必要がある。

cssコンパイルする前のディレクトリ構造は以下のとおり:

.
├── project.clj
├── resources
│   └── public
│       └── css
└── src
    └── min_garden
        └── core.clj

あとはシェルからlein runを実行:

$lein run
Wrote: resources/public/css/main.css

-main関数が走り、resources/public/cssに以下のmain.cssファイルが作成される:

body {
  font-size: 16px;
}

次回はこのコンパイルプロセスを自動化するlein-gardenの話。