Pythonで〇×ゲームのAIを一から作成する　その133　再定義した AI の検証と任意の AI が計算した候補手または評価値のゲーム盤への表示

# 目次と前回の記事

https://qiita.com/ysgeso/items/2381dd4e3283cbed49a0

https://qiita.com/ysgeso/items/b1cfaa9c4ec963d27820

## これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

| リンク | 説明 |
|:--|:--|
| [marubatsu.py](https://github.com/ysgeso/marubatsu/blob/master/133/marubatsu.py)  | Marubatsu、Marubatsu_GUI クラスの定義|
| [ai.py](https://github.com/ysgeso/marubatsu/blob/master/133/ai.py)  |  AI に関する関数 |
| [test.py](https://github.com/ysgeso/marubatsu/blob/master/133/test.py)  |  テストに関する関数 |
|  [util.py](https://github.com/ysgeso/marubatsu/blob/master/133/util.py) |  ユーティリティ関数の定義。現在は `gui_play` のみ定義されている |
| [tree.py](https://github.com/ysgeso/marubatsu/blob/master/133/tree.py)  | ゲーム木に関する Node、Mbtree クラスの定義  |
| [gui.py](https://github.com/ysgeso/marubatsu/blob/master/133/gui.py)  |  GUI に関する処理を行う基底クラスとなる GUI クラスの定義  |

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

https://qiita.com/ysgeso/items/10d1d01192c014173e4b

# 再定義した AI の検証

前回の記事では、`gui_play` を利用して __再定義した AI を検証__ すると説明しましたが、よく考えると __`ai_match`__ を使ったほうがより簡単に検証できることに気づきましたので、その方法で検証することにします。具体的には __再定義したそれぞれの AI と `ai2s` の間で対戦を行い__、__対戦結果を修正前の対戦結果と比較__ して、ほぼ同じであれば再定義した AI が正しい処理を行うことが高いことが検証できます。

## `ai1` ~ `ai7` の検証

まず、下記のプログラムで __`ai1` ~ `ai7` を検証__ します。

* __1 行目__：ai モジュールをインポートする
* __4 行目__：`i` を `1` から `7` まで 1 ずつ増やしながら繰り返す
* __5 行目__：組み込み関数 [`getattr`](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#getattr-%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F-ai-%E3%81%AE%E9%96%A2%E6%95%B0%E3%81%AE%E5%8F%96%E5%BE%97%E6%96%B9%E6%B3%95) を利用して、1 行目でインポートした ai モジュールから、`f"ai{i}"` という名前の AI の関数を取得して、`ai_i` に代入する
* __6 行目__：`ai_match` を利用して、`ai_i` と `ai2s` との対戦を行う。処理時間を短くするために、対戦回数を 1000 回とした

```python
1  import ai
2  from ai import ai_match
3    
4  for i in range(1, 8):
5      ai_i = getattr(ai, f"ai{i}")
6      ai_match(ai=[ai_i, ai.ai2s], match_num=1000)
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
import ai
from ai import ai_match
    
for i in range(1, 8):
    ai_i = getattr(ai, f"ai{i}")
    ai_match(ai=[ai_i, ai.ai2s], match_num=1000)
```
</details>

実行結果（実行結果はランダムなので下記と異なる場合があります）

```
ai1 VS ai2s
100%|██████████| 1000/1000 [00:03<00:00, 312.33it/s]
count     win    lose    draw
o         769     180      51
x         438     535      27
total    1207     715      78

ratio     win    lose    draw
o       76.9%   18.0%    5.1%
x       43.8%   53.5%    2.7%
total   60.4%   35.8%    3.9%

ai2 VS ai2s
100%|██████████| 1000/1000 [00:04<00:00, 238.41it/s]
count     win    lose    draw
o         595     274     131
x         276     597     127
total     871     871     258

ratio     win    lose    draw
o       59.5%   27.4%   13.1%
x       27.6%   59.7%   12.7%
total   43.5%   43.5%   12.9%

ai3 VS ai2s
100%|██████████| 1000/1000 [00:04<00:00, 249.27it/s]
count     win    lose    draw
o         684     207     109
x         339     511     150
total    1023     718     259

ratio     win    lose    draw
o       68.4%   20.7%   10.9%
x       33.9%   51.1%   15.0%
total   51.1%   35.9%   13.0%

ai4 VS ai2s
100%|██████████| 1000/1000 [00:03<00:00, 282.44it/s]
count     win    lose    draw
o         822      92      86
x         589     325      86
total    1411     417     172

ratio     win    lose    draw
o       82.2%    9.2%    8.6%
x       58.9%   32.5%    8.6%
total   70.5%   20.8%    8.6%

ai5 VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 159.67it/s]
count     win    lose    draw
o         787     139      74
x         526     382      92
total    1313     521     166

ratio     win    lose    draw
o       78.7%   13.9%    7.4%
x       52.6%   38.2%    9.2%
total   65.6%   26.1%    8.3%

ai6 VS ai2s
100%|██████████| 1000/1000 [00:09<00:00, 108.07it/s]
count     win    lose    draw
o         896      11      93
x         697      68     235
total    1593      79     328

ratio     win    lose    draw
o       89.6%    1.1%    9.3%
x       69.7%    6.8%   23.5%
total   79.7%    4.0%   16.4%

ai7 VS ai2s
100%|██████████| 1000/1000 [00:05<00:00, 170.71it/s]
count     win    lose    draw
o         950       2      48
x         827      30     143
total    1777      32     191

ratio     win    lose    draw
o       95.0%    0.2%    4.8%
x       82.7%    3.0%   14.3%
total   88.8%    1.6%    9.6%
```

下記は __[修正前](https://qiita.com/ysgeso/items/10d1d01192c014173e4b#%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E3%81%AA-ai-%E3%81%A7%E3%81%82%E3%82%8B-ai2-%E3%81%A8%E3%81%AE%E5%AF%BE%E6%88%A6%E7%B5%90%E6%9E%9C) と修正後の `ai2s` との対戦成績を比較した表__ です。左の数値が修正前、右の数値が修正後のものです。表から __すべての対戦成績__ が修正前と修正後で __ほとんど変わらない__ ので、__AI の再定義が正しく行われた可能性が高い__ ことが確認できます。

| AI | 〇 の勝率| × の勝率 | 引き分け率 |
|:-:|--:|--:|--:|
| __`ai1`__  | 61.4/60.4  | 34.5/35.8  | 4.1/ 3.9 |
| __`ai2`__  | 43.9/43.5  | 43.7/43.5  | 12.5/12.9 |
| __`ai3`__  | 54.1/51.1  | 33.4/35.9  | 12.5/13.0 |
| __`ai4`__  | 70.1/70.5  | 21.3/20.8  | 8.6/ 8.6 |
| __`ai5`__  | 66.5/65.6  | 26.0/26.1  | 7.4/ 8.3 |
| __`ai6`__  | 79.6/79.7  | 4.2/ 4.0  | 16.2/16.4 |
| __`ai7`__  | 89.0/88.8  | 1.3/ 1.6  | 9.7/ 9.6 |

## `ai1s` ~ `ai14s` の検証

次に、下記のプログラムで __`ai1s` ~ `ai14s`__ を検証します。プログラムは先ほどとほぼ同じなので説明は省略します。

```python
for i in range(1, 15):
     ai_is = getattr(ai, f"ai{i}s")
     ai_match(ai=[ai_is, ai.ai2s], match_num=1000)
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
-for i in range(1, 8):
+for i in range(1, 15):
-    ai_i = getattr(ai, f"ai{i}")
+    ai_is = getattr(ai, f"ai{i}s")
-    ai_match(ai=[ai_i, ai.ai2s], match_num=1000)
+    ai_match(ai=[ai_is, ai.ai2s], match_num=1000)

```
</details>

実行結果（実行結果はランダムなので下記と異なる場合があります）

```
ai1s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 164.07it/s]
count     win    lose    draw
o         764     190      46
x         440     526      34
total    1204     716      80

ratio     win    lose    draw
o       76.4%   19.0%    4.6%
x       44.0%   52.6%    3.4%
total   60.2%   35.8%    4.0%

ai2s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 147.85it/s]
count     win    lose    draw
o         567     300     133
x         311     559     130
total     878     859     263

ratio     win    lose    draw
o       56.7%   30.0%   13.3%
x       31.1%   55.9%   13.0%
total   43.9%   43.0%   13.2%

ai3s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 144.43it/s]
count     win    lose    draw
o         685     182     133
x         412     471     117
total    1097     653     250

ratio     win    lose    draw
o       68.5%   18.2%   13.3%
x       41.2%   47.1%   11.7%
total   54.9%   32.6%   12.5%

ai4s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 152.51it/s]
count     win    lose    draw
o         832      97      71
x         564     334     102
total    1396     431     173

ratio     win    lose    draw
o       83.2%    9.7%    7.1%
x       56.4%   33.4%   10.2%
total   69.8%   21.6%    8.6%

ai5s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 150.70it/s]
count     win    lose    draw
o         801     131      68
x         535     381      84
total    1336     512     152

ratio     win    lose    draw
o       80.1%   13.1%    6.8%
x       53.5%   38.1%    8.4%
total   66.8%   25.6%    7.6%

ai6s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 135.45it/s]
count     win    lose    draw
o         901      11      88
x         690      85     225
total    1591      96     313

ratio     win    lose    draw
o       90.1%    1.1%    8.8%
x       69.0%    8.5%   22.5%
total   79.5%    4.8%   15.7%

ai7s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 148.10it/s]
count     win    lose    draw
o         967       3      30
x         805      34     161
total    1772      37     191

ratio     win    lose    draw
o       96.7%    0.3%    3.0%
x       80.5%    3.4%   16.1%
total   88.6%    1.8%    9.6%

ai8s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 140.75it/s]
count     win    lose    draw
o         982       1      17
x         885      26      89
total    1867      27     106

ratio     win    lose    draw
o       98.2%    0.1%    1.7%
x       88.5%    2.6%    8.9%
total   93.3%    1.4%    5.3%

ai9s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 132.77it/s]
count     win    lose    draw
o         993       1       6
x         874      30      96
total    1867      31     102

ratio     win    lose    draw
o       99.3%    0.1%    0.6%
x       87.4%    3.0%    9.6%
total   93.3%    1.6%    5.1%

ai10s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 137.10it/s]
count     win    lose    draw
o         974       0      26
x         860      28     112
total    1834      28     138

ratio     win    lose    draw
o       97.4%    0.0%    2.6%
x       86.0%    2.8%   11.2%
total   91.7%    1.4%    6.9%

ai11s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 142.71it/s]
count     win    lose    draw
o         992       0       8
x         875       6     119
total    1867       6     127

ratio     win    lose    draw
o       99.2%    0.0%    0.8%
x       87.5%    0.6%   11.9%
total   93.3%    0.3%    6.3%

ai12s VS ai2s
100%|██████████| 1000/1000 [00:06<00:00, 147.33it/s]
count     win    lose    draw
o         986       0      14
x         899       0     101
total    1885       0     115

ratio     win    lose    draw
o       98.6%    0.0%    1.4%
x       89.9%    0.0%   10.1%
total   94.2%    0.0%    5.8%

ai13s VS ai2s
100%|██████████| 1000/1000 [00:07<00:00, 133.52it/s]
count     win    lose    draw
o         990       0      10
x         883       0     117
total    1873       0     127

ratio     win    lose    draw
o       99.0%    0.0%    1.0%
x       88.3%    0.0%   11.7%
total   93.7%    0.0%    6.3%

ai14s VS ai2s
100%|██████████| 1000/1000 [00:08<00:00, 124.52it/s]
count     win    lose    draw
o         989       0      11
x         891       0     109
total    1880       0     120

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       89.1%    0.0%   10.9%
total   94.0%    0.0%    6.0%
```

下記は [修正前](https://qiita.com/ysgeso/items/10d1d01192c014173e4b#%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E3%81%AA-ai-%E3%81%A7%E3%81%82%E3%82%8B-ai2-%E3%81%A8%E3%81%AE%E5%AF%BE%E6%88%A6%E7%B5%90%E6%9E%9C) と修正後の `ai2s` との対戦成績を比較した表です。左の数値が修正前、右の数値が修正後のものです。表からすべての対戦成績が修正前と修正後でほとんど変わらないので、__AI の再定義が正しく行われた可能性が高い__ ことが確認できます。

| AI | 〇 の勝率| × の勝率 | 引き分け率 |
|:-:|--:|--:|--:|
| __`ai1s`__  | 61.4/60.2  | 34.5/35.8  | 4.1/ 4.0 |
| __`ai2s`__  | 43.9/44.5  | 43.9/43.0  | 12.5/13.2 |
| __`ai3s`__  | 54.1/54.9  | 33.4/32.6  | 12.5/12.5
| __`ai4s`__  | 70.1/69.8  | 21.3/21.6  | 8.6/ 8.6 |
| __`ai5s`__  | 66.5/66.8  | 26.0/25.6  | 7.4/ 7.6 |
| __`ai6s`__  | 79.6/79.5  | 4.2/ 4.8  | 16.2/15.7 |
| __`ai7s`__  | 89.0/88.6  | 1.3/ 1.8  | 9.7/ 9.6 |
| __`ai8s`__  | 93.8/93.3  | 1.3/ 1.4  | 4.9/ 5.3 |
| __`ai9s`__  | 94.1/93.3  | 1.3/ 1.6  | 4.6/ 5.1 |
| __`ai10s`__  | 91.5/91.7  | 1.3/ 1.4  | 7.2/ 6.9 |
| __`ai11s`__  | 93.4/93.3  | 0.4/ 0.3  | 6.2/ 6.3 |
| __`ai12s`__  | 93.5/94.2  | 0.0/ 0.0  | 6.5/ 5.8 |
| __`ai13s`__  | 93.7/93.7  | 0.0/ 0.0  | 6.3/ 6.3 |
| __`ai14s`__  | 93.9/94.0  | 0.0/ 0.0  | 6.1/ 6.0 |

## `ai_gt1` の検証

__ゲーム木を利用した強解決 の AI__ である __`ai_gt1` ~ `ai_gt6`__ は、__同じゲーム木のデータを利用__ して着手を選択する AI なので、__`ai_gt1` のみ__ を下記のプログラムで検証することにします。

```python
from ai import ai_gt1
from tree import Mbtree

mbtree = Mbtree.load("../data/aidata")
ai_match(ai=[ai_gt1, ai.ai2s],
    params=[{"mbtree": mbtree}, {}], match_num=1000)
```

実行結果

```
ai_gt1 VS ai2s
100%|██████████| 1000/1000 [00:03<00:00, 309.15it/s]
count     win    lose    draw
o         961       0      39
x         784       0     216
total    1745       0     255

ratio     win    lose    draw
o       96.1%    0.0%    3.9%
x       78.4%    0.0%   21.6%
total   87.2%    0.0%   12.8%
```

下記は [修正前](https://qiita.com/ysgeso/items/10d1d01192c014173e4b#%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E3%81%AA-ai-%E3%81%A7%E3%81%82%E3%82%8B-ai2-%E3%81%A8%E3%81%AE%E5%AF%BE%E6%88%A6%E7%B5%90%E6%9E%9C) と修正後の `ai2s` との対戦成績を比較した表です。左の数値が修正前、右の数値が修正後のものです。表から __`ai_gt1` の再定義が正しく行われた可能性が高い__ ことが確認できます。

| AI | 〇 の勝率| × の勝率 | 引き分け率 |
|:-:|--:|--:|--:|
| __`ai_gt1`__  | 87.4/87.2  | 0.0/ 0.0  | 12.6/ 12.8 |

以上から、再定義した全ての AI が正しく行われた可能性が高いことが確認できました。

# AI が計算した候補手または評価値のゲーム盤への表示

これで AI の関数をすべて再定義し、それぞれが __仮引数 `analyze`__ を持ち、__候補手の一覧__ と、評価値を計算する AI の場合は __それぞれの合法手を着手した際の局面の評価値のデータ__ を __返すことができる__ ようになりました。

[以前の記事](https://qiita.com/ysgeso/items/2c76474ff139a824f4fd) で、__`ai3s`__ が計算した __評価値のゲーム盤への表示__ の処理を実装を行いましたが、AI の関数を上記のようにすべて再定義したことによって、__任意の AI__ が計算した __候補手または評価値のゲーム盤への表示__ を行うことができるようになったので、その実装を行います。

## Marubatsu_GUI クラスの `create_dropdown` の修正

まず、候補手などを計算する __AI を選択するための Dropdown を作成__ し、__画面に配置__ する必要があります。その __Dropdown に登録する AI__ は、__対戦を行う AI と同じ__ ものとし、下記のプログラムのように Marubatsu_GUI クラスの `create_dropdown` を修正して Dropdown を作成し、__`status_dropdown`__ という属性に代入することにします。

* __18 ~ 23 行目__：9 ~ 15 行の 〇 と × を担当する AI と同じ項目を持つ Dropdown を作成し、`status_dropdown` 属性に代入する。ただし、9 ~ 15 行目にあった、Dropdown の左に表示する説明文を表す `description` は必要ないので削除した

```python
 1  from marubatsu import Marubatsu_GUI
 2  import ipywidgets as widgets
 3
 4  def create_dropdown(self):
元と同じなので省略
 5      for i in range(2):
 6          # Dropdown の description を計算する
 7          description = "〇" if i == 0 else "×"
 8          self.dropdown_list.append(
 9              widgets.Dropdown(
10                  options=self.ai_dict,
11                  description=description,
12                  layout=widgets.Layout(width="100px"),
13                  style={"description_width": "20px"},
14                  value=select_values[i],
15              )
16          ) 
17            
18      self.status_dropdown = widgets.Dropdown(
19          options=self.ai_dict,
20          layout=widgets.Layout(width="100px"),
21          style={"description_width": "20px"},
22          value=select_values[0],
23      )
24    
25  Marubatsu_GUI.create_dropdown = create_dropdown
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
from marubatsu import Marubatsu_GUI
import ipywidgets as widgets

def create_dropdown(self):
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 〇 と × の Dropdown を格納する list
    self.dropdown_list = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        value = ( self.mb.ai[i], self.params[i] )
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[self.names[i]] = value

    for i in range(2):
        # Dropdown の description を計算する
        description = "〇" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        ) 
            
    self.status_dropdown = widgets.Dropdown(
        options=self.ai_dict,
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    
Marubatsu_GUI.create_dropdown = create_dropdown
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
from marubatsu import Marubatsu_GUI
import ipywidgets as widgets

def create_dropdown(self):
元と同じなので省略
    for i in range(2):
        # Dropdown の description を計算する
        description = "〇" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        ) 
            
+   self.status_dropdown = widgets.Dropdown(
+       options=self.ai_dict,
+       layout=widgets.Layout(width="100px"),
+       style={"description_width": "20px"},
+       value=select_values[0],
+   )
    
Marubatsu_GUI.create_dropdown = create_dropdown
```
</details>

## Marubatsu_GUI クラスの `display_widgets` の修正

次に、__`display_widgets`__ を下記のプログラムの 3 行目のように修正して、上記で作成した __Dropdown を「状況」ボタンの右に配置__ することにします。

```python
1  def display_widgets(self):
元と同じなので省略
2      # 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
3      hbox2 = widgets.HBox([self.show_status_button, self.status_dropdown, self.size_slider])
元と同じなので省略
4
5  Marubatsu_GUI.display_widgets = display_widgets
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def display_widgets(self):
    # 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button, 
                        self.show_tree_button, self.reset_tree_button, self.help_button])
    # 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
    hbox2 = widgets.HBox([self.show_status_button, self.status_dropdown, self.size_slider])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox3 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox4 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox4、Figure、Output を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3, hbox4, self.fig.canvas, self.output, self.help]))

Marubatsu_GUI.display_widgets = display_widgets
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def display_widgets(self):
元と同じなので省略
    # 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
-   hbox2 = widgets.HBox([self.show_status_button, self.size_slider])
+   hbox2 = widgets.HBox([self.show_status_button, self.status_dropdown, self.size_slider])
元と同じなので省略

Marubatsu_GUI.display_widgets = display_widgets
```
</details>

## Marubatsu_GUI クラスの `update_gui` の修正

次に、__`update_gui`__ を下記のプログラムのように修正して、__Dropdown で選択した AI が計算した評価値または候補手を表示__ するようにします。なお、修正の際に、上部に表示される __局面の状況__ に 〇、×、△ だけを表示するとわかりづらい気がしましたので、__「状況 〇」__ のように __マークの前に「状況」という文字を表示__ するように修正しました。

* __15 行目__：局面の状況を表示する際に、「状況」という文字を表示するように修正した
* __16 行目__：「状況」という文字を __リプレイモードで表示__ すると、__「Replay」という文字が右にはみ出してしまう__ ようになるので、7、8 行目と同様の方法で __[中央揃えで表示](https://qiita.com/ysgeso/items/903dc60e865ff56191fc#%E5%AF%BE%E6%88%A6%E3%82%AB%E3%83%BC%E3%83%89%E3%81%AE%E8%A1%A8%E7%A4%BA%E5%A0%B4%E6%89%80%E3%81%AE%E5%BE%AE%E4%BF%AE%E6%AD%A3)__ することで、はみ出さないようにした
* __19 行目__：Dropdown が選択中の項目の値から __AI の関数__ と、__AI のパラメーター__ を取り出して __`ai` と `params` に代入__ する
* __20 行目__：Dropdown に __「人間」の項目が選択__ されている場合は、__`ai` に `None` が代入__ されているので、`ai` に `None` が代入されていない場合に処理を行うようにする。この処理を記述し忘れると、人間の項目が選択される場合に 21 行目の処理でエラーが発生する
* __21 ~ 23 行目__：`ai` と `params` を利用して `score_by_move` と `candidate` を計算するように修正する
* __25 ~ 30 行目__：`ai` が `None` でない場合に評価値または候補手を表示するように修正する

```python
 1  from marubatsu import Marubatsu
 2  from copy import deepcopy
 3
 4  def update_gui(self):
元と同じなので省略
 5      # 上部のメッセージを描画する
 6      # 対戦カードの文字列を計算する
 7      ax.text(1.5, 3.5, f"{self.dropdown_list[0].label}　VS　{self.dropdown_list[1].label}", 
 8             fontsize=7*self.size, ha="center")   
 9
10      # ゲームの決着がついていない場合は、手番を表示する
11      if self.mb.status == Marubatsu.PLAYING:
12          text = "Turn " + self.mb.turn
13          score = self.score_table[self.mb.board_to_str()]["score"]
14          if self.show_status:
15              text += " 状況 " + calc_status_txt(score)
元と同じなので省略
16      ax.text(1.5, -0.2, text, fontsize=7*self.size, ha="center")
元と同じなので省略
17      if self.show_status:
18          bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
19          ai, params = self.status_dropdown.value
20          if ai is not None:
21              analyze = ai(self.mb, analyze=True, **params)
22              score_by_move = analyze["score_by_move"]
23              candidate = analyze["candidate"]
24          for move in self.mb.calc_legal_moves():
元と同じなので省略
25              if ai is not None:
26                  if score_by_move is not None:
27                      color = "red" if move in candidate else "black"
28                      ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
29                  elif move in candidate:
30                      ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
元と同じなので省略
31        
32  Marubatsu_GUI.update_gui = update_gui
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
from marubatsu import Marubatsu
from copy import deepcopy

def update_gui(self):
    def calc_status_txt(score):
        if score > 0:
            return "〇"
        elif score == 0:
            return "△"
        else:
            return "×"
    
    ax = self.ax

    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()

    # y 軸を反転させる
    ax.invert_yaxis()

    # 枠と目盛りを表示しないようにする
    ax.axis("off")   

    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label}　VS　{self.dropdown_list[1].label}", 
            fontsize=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        score = self.score_table[self.mb.board_to_str()]["score"]
        if self.show_status:
            text += " 状況 " + calc_status_txt(score)
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(1.5, -0.2, text, fontsize=7*self.size, ha="center")

    self.draw_board(ax, self.mb, lw=0.7*self.size)
    
    if self.show_status:
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
        ai, params = self.status_dropdown.value
        if ai is not None:
            analyze = ai(self.mb, analyze=True, **params)
            score_by_move = analyze["score_by_move"]
            candidate = analyze["candidate"]
        for move in self.mb.calc_legal_moves():
            x, y = move
            mb = deepcopy(self.mb)
            mb.move(x, y)
            score = self.score_table[mb.board_to_str()]["score"]
            color = "red" if move in bestmoves else "black"
            text = calc_status_txt(score)
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
            if ai is not None:
                if score_by_move is not None:
                    color = "red" if move in candidate else "black"
                    ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
                elif move in candidate:
                    ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
                
    self.update_widgets_status()

    if hasattr(self, "mbtree_gui"):
        from tree import Node

        self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
        self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
from marubatsu import Marubatsu
from copy import deepcopy

def update_gui(self):
元と同じなので省略
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label}　VS　{self.dropdown_list[1].label}", 
            fontsize=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        score = self.score_table[self.mb.board_to_str()]["score"]
        if self.show_status:
-           text += " " + calc_status_txt(score)
+           text += " 状況 " + calc_status_txt(score)
元と同じなので省略
-   ax.text(0, -0.2, text, fontsize=7*self.size)
+   ax.text(1.5, -0.2, text, fontsize=7*self.size, ha="center")
元と同じなので省略
    if self.show_status:
-       from ai import ai3
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
-       analyze = ai3(self.mb, analyze=True)
-       score_by_move = analyze["score_by_move"]
-       candidate = analyze["candidate"]
+       ai, params = self.status_dropdown.value
+       if ai is not None:
+           analyze = ai(self.mb, analyze=True, **params)
+           score_by_move = analyze["score_by_move"]
+           candidate = analyze["candidate"]
        for move in self.mb.calc_legal_moves():
元と同じなので省略
-           if score_by_move is not None:
-               color = "red" if move in candidate else "black"
-               ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
-           elif move in candidate:
-               ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
+           if ai is not None:
+               if score_by_move is not None:
+                   color = "red" if move in candidate else "black"
+                   ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
+               elif move in candidate:
+                   ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

## Marubatsu_GUI クラスの `create_event_handler` の修正

上記の修正で、AI の評価値または候補手が表示されるようになりますが、__Dropdown の項目を変更した際__ に __表示が更新されない__ という問題があります。そこで、__`create_event_handler`__ を下記のプログラムのように修正します。

* __8、9 行目__：__Dropdown が変更__ された際に呼び出される __イベントハンドラを定義__ する。__行う処理__ は、`update_gui` を呼び出して、__ゲーム盤の表示を更新する処理__ である
* __18 行目__：Dropdown とイベントハンドラを結び付ける

```python
 1  import math
 2
 3  def create_event_handler(self):
元と同じなので省略   
 4      def on_show_status_button_clicked(b=None):
 5          self.show_status = not self.show_status
 6          self.update_gui()
 7
 8      def on_status_dropdown_changed(changed):
 9          self.update_gui()
10
11      def on_size_slider_changed(changed):
12          self.size = changed["new"]
13          self.fig.set_figwidth(self.size)
14          self.fig.set_figheight(self.size)
15          self.update_gui()
16
17      self.show_status_button.on_click(on_show_status_button_clicked)
18      self.status_dropdown.observe(on_status_dropdown_changed, names="value")
19      self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
20
21  Marubatsu_GUI.create_event_handler = create_event_handler
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
import math

def create_event_handler(self):
    # 乱数の種のチェックボックスのイベントハンドラを定義する
    def on_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b=None):
        path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                        initialdir="save")
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                self.params = data["params"] if "params" in data else [ {}, {} ]
                if "names" in data:
                    names = data["names"]
                else:
                    names = [ "人間" if mb.ai[i] is None else mb.ai[i].__name__ for i in range(2)]                       
                options = self.dropdown_list[0].options.copy()
                for i in range(2):
                    value = (self.mb.ai[i], self.params[i]) 
                    if not value in options.values():
                        options[names[i]] = value
                for i in range(2):
                    self.dropdown_list[i].options = options
                    self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])            
                change_step(data["move_count"])
                if data["seed"] is not None:                   
                    self.checkbox.value = True
                    self.inttext.value = data["seed"]
                else:
                    self.checkbox.value = False
                    
    def on_save_button_clicked(b=None):
        names = [ self.dropdown_list[i].label for i in range(2) ]     
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{names[0]} VS {names[1]} {timestr}"
        path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                            initialdir="save", initialfile=fname,
                                            defaultextension="mbsav")
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                    "params": self.params,
                    "names": names,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
                
    def on_show_tree_button_clicked(b=None):
        self.show_subtree = not self.show_subtree
        self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
        self.update_gui()
        
    def on_reset_tree_button_clicked(b=None):
        self.update_gui()
                
    def on_help_button_clicked(b=None):
        self.help.layout.display = "none" if self.help.layout.display is None else None

    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    self.show_tree_button.on_click(on_show_tree_button_clicked)
    self.reset_tree_button.on_click(on_reset_tree_button_clicked)
    self.help_button.on_click(on_help_button_clicked)
    
    def on_show_status_button_clicked(b=None):
        self.show_status = not self.show_status
        self.update_gui()

    def on_status_dropdown_changed(changed):
        self.update_gui()

    def on_size_slider_changed(changed):
        self.size = changed["new"]
        self.fig.set_figwidth(self.size)
        self.fig.set_figheight(self.size)
        self.update_gui()

    self.show_status_button.on_click(on_show_status_button_clicked)
    self.status_dropdown.observe(on_status_dropdown_changed, names="value")
    self.size_slider.observe(on_size_slider_changed, names="value")

    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
        self.mb.play_loop(self, self.params)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        self.output.clear_output()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.update_gui()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   

    # step 手目の局面に移動する
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.update_gui()        

    def on_first_button_clicked(b=None):
        change_step(0)

    def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        if self.mb.move_count != changed["new"]:
            change_step(changed["new"])
        
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")

    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            with self.output:
                self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
                self.mb.play_loop(self, self.params)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
            "-": on_load_button_clicked,            
            "l": on_load_button_clicked,            
            "+": on_save_button_clicked,            
            "s": on_save_button_clicked,            
            "*": on_help_button_clicked,            
            "h": on_help_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                event.inaxes = True
                event.xdata = num % 3
                event.ydata = 2 - (num // 3)
                on_mouse_down(event)
            except:
                pass
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)  

Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
import math

def create_event_handler(self):
元と同じなので省略   
    def on_show_status_button_clicked(b=None):
        self.show_status = not self.show_status
        self.update_gui()

+   def on_status_dropdown_changed(changed):
+       self.update_gui()

    def on_size_slider_changed(changed):
        self.size = changed["new"]
        self.fig.set_figwidth(self.size)
        self.fig.set_figheight(self.size)
        self.update_gui()

    self.show_status_button.on_click(on_show_status_button_clicked)
+   self.status_dropdown.observe(on_status_dropdown_changed, names="value")
    self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略

Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

## `gui_play` の検証

上記の修正後に、下記のプログラムで `gui_play` を実行し、状況ボタンの右の __Dropdown に `ai1s` を選択__ して __「状況」ボタンをクリック__ すると、実行結果のようにそれぞれのマスに __`ai1s` が計算した評価値__ が、__[前回の記事](https://qiita.com/ysgeso/items/b1cfaa9c4ec963d27820#ai1s-%E3%81%AE%E4%BF%AE%E6%AD%A3)__ で __再定義した `ai1s` が計算した値と同じものが表示__ されることが確認できます。また、__最も評価値の高い (0, 0)__ のマスの __評価値の色__ が __`ai1s` の候補手__ として __赤色で表示される__ ことが確認できます。また、今回の記事の修正で、ゲーム盤の上部に「状況」という文字が表示されるようになったことも確認できます。

```python
from util import gui_play

gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/541d99fb-6377-092f-315e-34feeface28a.png" width="200">

また、Dropdown に __`ai14s`__、__`ai_gt6`__、__`人間`__ を選択した場合は、左から順に下図のようになります。`ai14s` は真ん中の (1, 1) を候補手としますが、__その理由__ が評価値が表示されることによって __わかりやすくなります__。また、この機能を実装したことで、新しく AI を実装した際に、__AI が計算した評価値を容易に確認できる__ ようになるというメリットが得られます。

`ai_gt6` は評価値を計算しないので、候補手という文字が、人間は AI ではないので何も表示されなくなります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/cbc75fee-4bf2-717a-9fd2-d212e7b7ba32.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/73544a06-ae06-1731-3d76-a7e769ebdd10.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4dd18878-71d9-5f62-653f-2c4e4e285334.png" width="200">

また、リプレイモードにした場合に、下図のようにゲーム盤の上部の文字が中央揃えで表示されることで、はみでないことが確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/c1d49182-1178-1da8-ff1c-651d38af0b84.png" width="200">

# 今回の記事の内容

今回の記事では、再定義した AI の関数の検証を行い、任意の AI が計算した候補手または評価値のゲーム盤への表示を行えるようにしました。

# 本記事で入力したプログラム

| リンク | 説明 |
|:--|:--|
| [marubatsu.ipynb](https://github.com/ysgeso/marubatsu/blob/master/133/marubatsu.ipynb)  | 本記事で入力して実行した JupyterLab のファイル |
| [marubatsu.py](https://github.com/ysgeso/marubatsu/blob/master/133/marubatsu_new.py)  | 本記事で更新した marubatsu_new.py  |

# 次回の記事

https://qiita.com/ysgeso/items/368f00bae5408bac496a