3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

関数型言語ClojureでAI将棋モバイルアプリをつくれないか

Last updated at Posted at 2019-06-05

宣伝

関ヶ原将棋Online
AppStoreで絶賛配信中

スクリーンショット 2018-11-16 11.38.38.png


背景

  • iOS版で出している将棋アプリのAndroid版を作りたい
  • Clojureで実装したい
  • ReactNative + ClojureScript でそれが実現可能に
  • re-natal

将棋AIのアルゴリズム

前段として必要なもの

  • 評価値
    • 局面自体を何かしらの基準で測定し評価した値。
    • 例えば駒得(どちらのユニットが多いか)・駒の働き・玉の硬さなど。
  • 評価関数
    • 局面に適用して評価値を出す関数。

ツリー上のデータを考える
1局面にN通りの指し手があって、その数だけ次局面が存在。


  • ミニマックス法
  • ネガマックス法
  • アルファ・ベータ法
  • ネガアルファ法

  • ミニマックス法
    • お互い最前手を指すと言う前提で考える。
  • ネガマックス法
    • ミニマックス法の改良版。評価値の判断を逆転することでより簡潔に記述できる。

  • アルファ・ベータ法
    • ミニマックス法の改良版。絶対に選ばれない局面を考慮してその局面評価処理をカットする(枝刈り)
  • ネガアルファ法
    • ネガマックス法の枝刈りバージョン。

nega-max.png


ネガマックス(Clojure)

(defn deep-score [{:keys [turn] :as db} depth]
  (if (= depth 0)
    (score db)
    (->> db
         next-dbs
         (map #(deep-score % (dec depth)))
         (sort (if turn > <))
         first)))

nega-alpha.png


ネガアルファ(Swift)

 func deepScore(_ kifu: String, size: Int, best: (String, Int)?, depth: Int) -> (String, Int) {
        let gameData_ = ShogiLogic.gameData(kifu, size: size)
        if (gameData_["winner"] as? String) != nil { return (kifu, score(kifu, size: size)) }
        let turn = gameData_["turn"] as! String

        var result: (String, Int)?

        // 深さ0ならその局面の評価値
        if depth < 1 { result = (kifu, score(kifu, size: size)) }

        let children = movedKifus(kifu, size: size, stock: depth > 0, capture: depth < 1, safeGyoku: false).sorted {
            turn == "+" ? score($0, size: size) > score($1, size: size) : score($0, size: size) < score($1, size: size)
        }

        if children.count == 0 { return (kifu, score(kifu, size: size)) }

        // 1手先の局面群について
        for child in children {
            // 子供の評価値
            let score_ = deepScore(child, size: size, best: result, depth: depth - 1)

            // resultより高評価ならその値をresultにする(先手か後手かで評価軸を逆にする)
            if result == nil || (turn == "+" ? score_.1 > result!.1 : score_.1 < result!.1) {
                result = score_
            }
            //子供の評価値が同列な層の他局面より良かった場合に終了
            if best != nil && (turn == "+" ? result!.1 >= best!.1 : result!.1 <= best!.1) {
                return result!
            }
        }

        if result == nil { result = (kifu, score(kifu, size: size)) }
        return result!
    }
 }

ネガアルファ(Clojure)

(defn deep-score [{:keys [turn] :as db} depth best]
  (if (= depth 0)
    {:score (score db) :db db}
    (reduce (fn [{:keys [end score] :as current} next-db]
              (if end
                current
                (let [{target :score} (deep-score next-db (dec depth) score)]
                  (cond
                    (and ((complement nil?) best) ((if turn >= <=) target best)) {:end true :score target :db next-db}
                    (or (nil? score) ((if turn >= <=) target score)) {:end false :score target :db next-db}
                    :else current))))
            {:end false :score nil :db nil}
            (next-dbs db))))

前回まとめ

  • mapで処理する場合は途中の計算結果を次の計算に反映させられない
  • reduceで終了フラグ有無を持ち回した
  • 遅延評価を使えばもっとスマートに計算省略ができるのでは?
  • よくよく考えればreduceで持ち回さずとも再起でできそう(できた)

ネガアルファ(Clojure再起in再起)


(defn deep-score [{:keys [turn] :as db} depth same-layer-best]
  (if (= depth 0)
    {:score (score db) :db db}
    (loop [children (next-dbs db) {:keys [score] :as result} nil]
      (if (= (count children) 0)
        result
        (let [child (first children)
              child-score (:score (deep-score child (dec depth) result))]
          (if (and ((complement nil?) same-layer-best)
                   ((if turn >= <=) child-score same-layer-best))
            {:score child-score :db child}
            (recur
              (rest children)
              (if (or (nil? result)
                      ((if turn > <) child-score score))
                {:score child-score :db child}
                result))))))))


COMの指し手の計算中にハング

重い計算をメインスレッドでやってはいけない

ReactNativeはJSの仕組みを踏襲した世界観なので(基本的に)マルチスレッド非対応


NativeModules

自作のネイティブクラスをJSから呼び出して使える
(ネイティブではマルチスレッドでの処理は簡単に書けるので、これを使えばいい感じにできるのでは?)
(結論としてはAndroidでは無理でした)


流れとしては
ネイティブ側でReactNativeに晒すメソッドを定義
それをClojureScript(JS)側で呼び出す

(ThreadExampleというクラス名・executeというメソッド名でJS側に公開した場合)



(def thread (.-ThreadExample (.-NativeModules (js/require "react-native"))))

(.execute thread do-somethig-on-sub do-something-on-main)


iOSのマルチスレッド

Grand Central Dispatch (GCD)

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // Call long-running code on background thread
    dispatch_async(dispatch_get_main_queue(), ^{
    // Call running code on main thread
    });
});

NativeModulesにすると

RCT_EXPORT_METHOD(execute:(RCTResponseSenderBlock)main
                      sub:(RCTResponseSenderBlock)sub)
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sub(@[...]);
        dispatch_async(dispatch_get_main_queue(), ^{
            main(@[...]);
        });
    });
}

Androidのマルチスレッド

private class ThreadTask extends AsyncTask<String, Void, String> {
  protected String doInBackground(String... strs) {
    // Do something on Subthread
    return "done";
  }

  protected void onPostExecute(String str) {
    // Do something on MainThread
  }
}

new ThreadTask().execute("arg");

NativeModulesにすると

@ReactMethod
public void execute(Callback sub, Callback main) {
  new ThreadTask().execute(sub, main);
}

private class ThreadTask extends AsyncTask<Callback, Void, Callback> {
  protected String doInBackground(Callback... callbacks) {
    callbacks[0].invoke();
    return callbacks[1];
  }

  protected void onPostExecute(Callback main) {
    if (main != null) main.invoke();
  }
}


これでうまくいくように思われた。


しかし・・・。


Callbackの制約

ReactNative側からNativeModule側に1メソッドで与えたCallback群はどれか一つしか呼ぶことはできない
(Androidでのみ確認)


ので、Callbackを一つだけ受け取るメソッドに変更

 @ReactMethod
  public void executeSub(Callback callback) {
    new SubThreadTask().execute(callback);
  }

  @ReactMethod
  public void executeMain(Callback callback) {
    new MainThreadTask().execute(callback);
  }

  private class SubThreadTask extends AsyncTask<Callback, Void, String> {
    protected String doInBackground(Callback... callbacks) {
      callbacks[0].invoke();
      return "done";
    }

    protected void onPostExecute(String str) {
      // メインスレッドで何もしない
    }
  }

  private class MainThreadTask extends AsyncTask<Callback, Void, Callback> {
    protected Callback doInBackground(Callback... callbacks) {
      return callbacks[0]; // 実行せずにonPostExecuteにパス
    }

    protected void onPostExecute(Callback callback) {
      callback.invoke();
    }
  }

}

これでうまくいくように思われた。


しかし・・・。


サブスレッドで呼んでいるはずの処理がメインで実行される。
AndroidのNativeModulesはサブスレッド非対応っぽい


というわけでAndroidで非同期処理をやるならWebworkers的な記述をしなければいけない。

  1. AI部分をClojureScriptからJSにWorkerJSとして別途コンパイル(最適化が入るのでよくわからない・・)
  2. 謎の形にコンパイルされたJSにWorkerの機能を実装
  3. WorkerJSからClojureScriptで書かれた将棋ロジックのAPIにアクセス

と、どうしていいのかわからない課題が残された


まとめ

ReactNative大変(特にAndroid)

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?