自作したリバーシAIでEdaxに挑む！

# はじめに
どうも、y-tetsuです。

本記事では、Pythonで自作したリバーシ(オセロ)AIを、**Edax**(強豪AIの1つ)と対戦させて、世界レベルの強さを体感してみる！というものです。


3年ほど前、日経ソフトウェア様の[リバーシAI特集](https://info.nikkeibp.co.jp/media/NSW/atcl/mag/091700025/)をきっかけに、Python(Cython)を使ってリバーシAIを自分なりに作り、少しずつを改良を加えてきました。そしてついに機械学習は用いないまでも、古典的でヒューリスティックな手法にて**自分を超えるリバーシAI**を作るに至りました。


こうして出来上がった私のAIは、結構いい線いってたりしないかな？？ということを確かめるべく、今回、Edaxさんにご協力いただくことにしました。


# Edaxとは
Edaxとは最強クラスと目されているリバーシAIの一つです。対戦相手として不足なし、…というより恐れ多い。

(以下のページで、紹介されていました)
https://scorenow.mystrikingly.com/blog/edax

ソースコードや実行できるアプリは大変有難いことに、githubで公開されています。
https://github.com/abulmo/edax-reversi

また、様々な方がEdaxに関連する記事を書かれているのをお見掛けしました。
https://choi.lavox.net/edax/start
https://qiita.com/tanaka-a/items/6d6725d5866ebe85fb0b


# 自作AIについて
PythonとCython(高速化のため)を使って作りました。いろいろなサイトを漁りながら、強さに寄与しそうな評価指標を見つけては実装し、うまくいったものを寄せ集めた、というビットボードを使った代物です。これでも人間相手にはそこそこ頑張っているのではと自負しています。

## 序盤
簡単な定石を登録していて、登録されている局面においては定石どおりに打ちます。

- 鼠定石
- 牛定石
- 虎定石
- 兎定石
- 猫定石

など。(干支とは限らないんですね…)
	
## 中盤
以下の観点で重み付けした評価関数を用いて、**8手先**の局面を評価し、最も評価の良い手を選びます。

- 残りのマスが空いていても、**勝ち**が見えた手は評価を上げる
- 手を打つことができる**着手可能数**が、自分は多く相手が少なくなるほど評価を上げる
- 盤面の角、角の横や"X"といわれる場所など、8x8の**マスの位置**による優劣を重みづけして、現在の盤面から総合的に判断する
- 角から連続する、決してひっくり返されることがない**確定石**が多いほど評価を上げる
- 自分の石がなるべく空いているマスに接しない(相手の内側に入り込む)ほど評価を上げる

## 終盤
**残り16手**からは終局まで読み切り、石差の最も多くなる手を選びます。探索深さ16手は、動作確認をしてみて、恐らくこれなら**現実的な時間に結果を返すだろう**という深さを経験的に設定しました。(20手以上は当たり前のように読む世界レベルのハードルに既に達してませんが…)

このような自作AIを、勝手ながら「Blank8_16」と名付けてかわいがっていました。


# 対戦の準備
手前味噌で申し訳ないのですが、「PythonにてリバーシAIを自作したり、自作したAI同士を対戦させたりする」というのを割と簡単に行える、というのが売りのPythonのライブラリ(自作)を用いて、Edaxさんにお手合わせいただきました。

私のPCの関係上、Windows環境での説明となります。あらかじめご了承下さい。

## Edaxのダウンロード
まずは、本家サイトの以下より、Edax本体のアプリケーションと評価値算出用のデータをダウンロードして下さい。

https://github.com/abulmo/edax-reversi/releases

![edax_download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/028b0e6e-8da9-da01-5d88-5c4666f6d153.png)

上図の下線で示したファイルをダウンロードして下さい。

## reversiをインストールする
Python3.7をインストールし、以下のコマンドを実行して、**reversi**ライブラリをインストールしてください。
```
$ py -3.7 -m pip install git+https://github.com/y-tetsu/reversi
```

Python3.7以降でも問題なく動くと思いますが、その際はCythonのコンパイルの関係で、[Microsoft Visual C++](https://visualstudio.microsoft.com/ja/downloads/?utm_medium=microsoft&utm_source=docs.microsoft.com&utm_campaign=button+cta&utm_content=download+vs2019+rc)のインストールを併せて実施願います。

## EdaxをPythonから扱うためのコードを用意する
**reversi**ライブラリを使って、PythonからEdaxを実行するためのコードです。オリジナルのEdaxの実行とその出力をラップして、**reversi**ライブラリのAIとして扱うためのインターフェースに準拠させました。

```edax_wrap.py
import subprocess

from reversi import strategies
from reversi.strategies import AbstractStrategy
from reversi.move import Move


class Edax(AbstractStrategy):
    def next_move(self, color, board):
        if board.size != 8:
            return strategies.Random().next_move(color, board)

        with open('./board.txt', 'w') as f:
            f.write(board.get_board_line_info(color))

        cmd = "./edax-4.4 -solve ./board.txt"
        output_str = subprocess.run(cmd, capture_output=True, text=True).stdout
        move = output_str.split('\n')[2][57:].split()[0]

        return Move(move)
```		

### 盤面のサイズ制限
**reversi**のアプリケーションは盤面のサイズを4～26まで変えることができます。今回は8に限定するため、それ以外の場合はランダムに打つようにしています。

```python
if board.size != 8:
    return strategies.Random().next_move(color, board)
```

### 盤面のファイル出力
**reversi**の盤面情報を、Edaxの入力となる盤面情報へ変換しファイル(*board.txt**)に出力しています。
```python
with open('./board.txt', 'w') as f:
    f.write(board.get_board_line_info(color))
```

### Edaxの実行
subprocessによりEdaxを実行し、Edaxの標準出力をテキストで受け取っています。

`cmd`変数にEdax実行時のコマンドを記載しています。
```python
cmd = "./edax-4.4 -solve ./board.txt"
output_str = subprocess.run(cmd, capture_output=True, text=True).stdout
```

`-solve <ファイル名>`で盤面情報を読み込み、打ち手を標準出力で返してくれるようです。(細かなオプションは調べ切れておりません、申し訳ありません)

### 打ち手を返す
Edaxが標準出力した打ち手('A1'などの文字列)を取り出し、`Move`オブジェクトに変換することで**reversi**で扱える打ち手に変換しています。
```python
move = output_str.split('\n')[2][57:].split()[0]

return Move(move)
```

(Edaxの標準出力例)
```
 # | depth|score|       time   |  nodes (N)  |   N/s    | principal variation
---+------+-----+--------------+-------------+----------+---------------------
  1|21@73%  +01        0:00.547       2141770    3915484 B3 c2 B4 c6 D2 e6 B5
------+-----+--------------+-------------+----------+---------------------
./board.txt: 2141770 nodes in  0:00.547 ( 3915484 nodes/s).
1 positions; 0 erroneous move; 0 erroneous score; mean absolute score error = 0.000; mean absolute move error = 0.000
```

(コード処理内容)
1. 上記の2行目(0はじまり)を取り出す
2. 1.からさらに、57文字め以降(B3 c2 …の部分)を取り出す
3. 2.から、スペース区切りで配列に変換する
4. 3.の最初の要素をとりだし、Edaxの次の**打ち手**とする
5. 4.を`Move`オブジェクトにより**reversi**ライブラリで扱える打ち手の形式に変換

**reversi**で扱える打ち手の形式は、`(x, y)`のタプル形式となっています。


## 対戦を観戦するためのアプリ
tkinter製のアプリケーションに、自作AI(Blank8_16)とEdaxを追加したものです。

`# ↓↓↓↓↓ Add players here ↓↓↓↓↓`のところに参加させたいAIを記載します。

```reversi_app.py
from reversi import Reversi, strategies
from edax_wrap import Edax


if __name__ == '__main__':
    Reversi(
        # ↓↓↓↓↓ Add players here ↓↓↓↓↓
        {
            'Blank8_16': strategies.SwitchJ_Blank8_EndGame16(),
            'Edax': Edax(),
        }
        # ↑↑↑↑↑ Add players here ↑↑↑↑↑
    ).start()
```
		
## シミュレータコード
AI同士、先手と後手を入れ替えて、複数回の対戦結果を集計するためのコードです。

観戦アプリ同様、`# ↓↓↓↓↓ Add players here ↓↓↓↓↓`のところに参加させたいAIを記載します。

```reversi_simulator.py
import timeit
from reversi import Simulator, strategies
from edax_wrap import Edax


if __name__ == '__main__':
    simulator = Simulator(
        {
            # ↓↓↓↓↓ Add players here ↓↓↓↓↓
            'Blank8_16': strategies.SwitchJ_Blank8_EndGame16(),
            'Edax': Edax(),
            # ↑↑↑↑↑ Add players here ↑↑↑↑↑
        },
        './simulator_setting.json',
    )

    elapsed_time = timeit.timeit('simulator.start()', globals=globals(), number=1)
    print(simulator, elapsed_time, '(s)')

    if simulator.processes == 1:
        keys = strategies.Measure.elp_time.keys()
        for key in keys:
            print()
            print(key)
            print(' min :', strategies.Measure.elp_time[key]['min'], '(s)')
            print(' max :', strategies.Measure.elp_time[key]['max'], '(s)')
            print(' ave :', strategies.Measure.elp_time[key]['ave'], '(s)')
```

また、シミュレーション時の設定内容を合わせて以下の様に用意します。
```
{
    "board_size": 8,
    "board_type": "bitboard",
    "matches": 10,
    "processes": 1,
    "prallel": "player",
    "random_opening": 8,
    "player_names": [
        "Blank8_16",
        "Edax"
    ]
}
```
よく触るところだけピックアップします。(詳細は後述の**reversi**のページをご参照下さい)
- matches : 先手番、後手番それぞれの対戦回数
- random_opening : 開始からランダムに打つ手数。今回は8手までランダムに打った後、実AIで対戦させました。
- player_names : 参加AI


## フォルダ構成
これまでに、ダウンロードしたファイルや、用意したPythonコードは以下の配置として下さい。
```
C:.
│ edax-4.4               … Edax実行アプリケーション
│ edax_wrap.py           … PythonからEdaxを扱うためのPythonコード
│ reversi_app.py         … AIの対戦を観戦できるPythonアプリケーション
│ reversi_simulator.py   … AIの対戦をシミュレーションできるPythonアプリケーション
│ simulator_setting.json … シミュレータ設定ファイル
│
└─data                  … Edaxの評価値とBook
        book.dat
        eval.dat
```

これにて、対戦の準備は整いました。いよいよ結果を見てみましょう！


# 対戦の様子を観戦する
以下を実行すると、tkinterで作ったGUIアプリケーションが起動します。
```
py -3.7 reversi_app.py
```

実行の様子を以下に示します。
![play.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/8e611ff6-33ef-b749-a8f7-041590a481c5.gif)
Black"と"White"それぞれのAIを指定し、"Click to start"を実行して対戦の様子をご観戦下さい。(AIに"User"を指定した場合は人が操作できます)

# 複数回戦わせた結果を出す
以下を実行すると、シミュレータ設定に応じたシミュレーションを実行します。
```
py -3.7 reversi_simulator.py
```

先手・後手を10試合ずつ、開始8手はランダムに打つ条件での対戦結果は以下となりました。
```
Blank8_16 Edax
    - Blank8_16 Edax 5
    - Blank8_16 Edax 10
Edax Blank8_16
    - Edax Blank8_16 5
    - Edax Blank8_16 10

Size : 8
                          | Blank8_16                 Edax
-------------------------------------------------------------------------------
Blank8_16                 | ------                      0.0%
Edax                      | 100.0%                    ------
-------------------------------------------------------------------------------

                          | Total  | Win   Lose  Draw  Match
------------------------------------------------------------
Blank8_16                 |   0.0% |     0    20     0    20
Edax                      | 100.0% |    20     0     0    20
------------------------------------------------------------
```

先手・後手それぞれ10回ずつの結果です。…現実は厳しい。気持ちいいくらいですね。**お疲れ様でした！！**


# おわりに
まあ、お恥ずかしいことに完敗でした。

このような圧倒的強さのEdax様よりも、さらに強いと言われるEgaroucid5様もまだ控えておられるという事ですので、オセロAIの世界もまだまだ上には上がいるんだなと、感慨深いものがあります。

自分で作ったAIを、世界的に強いAIと一緒に戦わせることができる環境が、ものすごく簡単に用意できるという事に、素直に有難さを感じます。

Python以外のコードでも、インターフェースさえ合わせれば、オセロAIの熱き戦いに参加できますので、もしよかったらPython製ライブラリ**reversi**を何卒よろしくお願いします。

https://github.com/y-tetsu/reversi

それでは皆様、よきリバーシ・ライフを！


# 補足 : Edaxのビルド
Edaxのアプリケーションについて、本家からダウンロードしたものが動かない事があり(原因は不明ですが)、ビルドしなおしてうまくいったケースがありましたので、覚書を残しておきます。

## 1. MinGWのインストール
以下を参照しました。
https://qiita.com/kzrashi/items/4e0ab5949b69d4b333dd

## 2. makeのインストール
以下を参照しました。
https://qiita.com/avimigo/items/f501f44a8d44e3fd6987

## 3. Edaxのソースをダウンロードと、zip解凍
Edaxのソースをダウンロードし、zip解凍してください。
![edax_source_download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/93ea8992-0a55-4f9e-2405-872fc877416e.png)

上図の下線で示したファイルをダウンロードして下さい。

## 4. binフォルダ作成
アプリケーションの実行体作成用にbinフォルダを用意します。
```
edax-reversi-4.4
  bin/      ← 作る
  include/
  src/
  .hgignore
  README.md
```

## 5. srcフォルダに移動
scrフォルダに移動してください。

## 6. ビルド実行
以下を実行してください。(Windows用)
```
make build ARCH=x64 COMP=gcc OS=windows
```

## 5. アプリケーションの生成
binフォルダ以下に、"wEdax-x64.exe"が生成せれると思います。

本記事のコードで動かすためには、"wEdax-x64.exe"を"edax-4.4"にファイル名を変更(拡張子も削除)した後、所定のフォルダに置いて下さい。