Pythonで〇×ゲームのAIを一から作成する　その134　評価値を表示するAIの自動選択とゲーム木を利用するAIの評価値の一覧の計算

# 目次と前回の記事

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

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

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

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

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

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

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

# 今回の記事の内容と用語の定義

前回の記事では、Dropdown によって、任意の AI が計算した候補手または評価値のゲーム盤への表示できるようにしました。今回の記事では、この機能と `ai_gt6` の改良を行うことにします。

## 用語の定義

説明を短くするために、以後の記事で用いるいくつかの用語を定義することにします。

### AI の評価値の表示

「AI が計算した候補手または評価値のゲーム盤への表示」という表記は長いので、以後は「__AI の評価値の表示__」と記述することにします。「評価値の表示」としか表記しませんが、評価値を計算しない AI の場合は候補手を表示する点に注意して下さい。

### 評価値の Dropdown と 手番の Dropdown

現状では、Marubatsu_GUI クラスは上にある __評価値の表示を行う AI を選択__ する Dropdown と、下に 2 つある __手番を担当する AI を選択__ する Dropdown を作成します。以後は、前者の Dropdown を「__評価値の Dropdown__」、後者の Dropdwon を「__手番の Dropdown__」と表記することにします。

# バグの修正

今回の話を始める前に、__util.py にバグ__ がある事が判明したので修正します。

具体的には、[以前の記事](https://qiita.com/ysgeso/items/0f83bcdf76bac5b0175f#gui_play-%E3%81%B8%E3%81%AE-ai-%E3%81%AE%E7%99%BB%E9%8C%B2)で `gui_play` の下記の 4、5 行目のプログラムを追加して、__手番の Dropdown に `ai_gtsv` の項目を追加__ したのですが、その __次の回からの util.py にその内容が反映されていない__ ことが判明しました。

```python
1  def gui_play(ai=None, params=None, ai_dict=None, seed=None):
略
2          bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
3          ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
4          bestmoves_by_board_sv = load_bestmoves("../data/bestmoves_by_board_shortest_victory.dat")
5          ai_dict["ai_gtsv"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board_sv})
```

このバグによって、その回以降から前回の記事までで、__`gui_play()` を実行した際に `ai_gtsv` が項目に登録されない__ というバグが発生しています。ただし、このバグをさかのぼって修正すると、記事の画像と、実際のプログラムの実行が合わなくなる点と、このバグによって記事の趣旨が大きく変わることはないので、過去の記事をさかのぼってプログラムを修正しないことにし、__今回の記事の util.py から修正__ することにします。

# 評価値を表示する AI の自動選択

現状では評価値の Dropdown で選択された AI が計算した評価値が表示されますが、__AI どうしの対戦結果__ を __リプレイモードで検証__ する際には、それぞれの局面で、その __手番を担当する AI が計算した評価値__ が __自動的に表示__ されると便利です。

そのような機能を追加するための __[ユーザーインターフェース（UI）](https://qiita.com/ysgeso/items/1b7b1abe942e2d10c0b4#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9)__ について少し考えてみて下さい。

## UI の検討

簡単に思いつく方法としては、__ON にする__ ことで __手番を担当する AI が評価値の計算を行う__ ようになる __Button、Checkbox、[Togglebutton](https://qiita.com/ysgeso/items/71a02346a63d6de28db4#togglebutton-%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%A3%9C%E8%B6%B3) などのウィジェットを追加__ するという方法があるでしょう。ただし、ウィジェットを追加すると、その __ウィジェットを表示するための場所が必要__ になります。また、ウィジェットを増やすと __UI の見た目が複雑__ になり、__操作方法がわかりづらくなる__ という問題も発生します。

他の方法として、評価値の Dropdown の __項目に__、現在の手番の AI を利用するという意味を表す __「手番の AI」という項目を追加する__ という方法が考えられます。こちらの方法であれば、__新しいウィジェットが増えない__ ので、ウィジェットを追加する方法でおきる問題は発生しません。一方、__Dropdown__ は、普段は複数ある項目の中から __1 つの項目しか表示しない__ ので、__「手番の AI」という項目の存在が気づかれなくなる__ 可能性が高いという欠点があります。その点、Button などのウィジェットは、__常に表示されている__ ので、そのような機能が存在することが常にわかるようになります。

上記の 2 種類の UI の __どちらを採用するか__ は、それぞれの __利点と欠点を考慮して検討する__ 必要があります。__本記事__ では、後者の新しいウィジェットを配置する必要がなく、場所をとらないという利点を重視して、__Dropdown に項目を追加するという方法を採用__ することにします。前者の方法がふさわしいと思った方や、他にももっとふさわしい UI を思いついた人はその UI をぜひ実装してみて下さい。

## Marubatsu_GUI クラスの `create_dropdown` メソッドの修正

Dropdown に「手番の AI」という項目を追加する際には、Dropdown を作成する際に記述する __項目のデータを表す dict__ に「手番の AI」の __項目のデータを設定__ する必要があります。どのようなデータを設定すればよいかについて少し考えてみて下さい。

### 「手番の AI」の項目の値の検討

他の項目では、__項目の値__ に以下のようなデータを設定しています。

* __`(AI の関数, AI のパラメーター)`__ という tuple を設定する
* ただし、手番を人間が担当する場合は __`(None, None)`__ という tuple を設定する

__「手番の AI」__ の場合は、__AI の関数を直接設定することはできない__ ので、人間の項目のように、設定する tuple をどのように設定するかを考える必要があります。

__tuple の 0 番の要素__ に設定する内容は、__他の項目の 0 番の要素の値と区別__ できれば、どのような値を設定してもかまいません。そこで本記事では __自動的（auto）__ に手番の AI を利用するということから、__`"Auto"` という文字列を設定__ することにします。他のデータを設定したい人は自由に変更して下さい。

「手番の AI」の場合は、__AI のパラメータの情報を直接設定することはできない__ ので、__tuple の 1 番の要素__ には、人間が担当する場合と同様に __`None` を設定__ することにします。なお、tuple の 1 番の要素に設定するデータは __実際のプログラムで利用することはない__ のでどのような値を設定してもかまいません。

上記から、__「手番の AI」の項目の値__ には __`("Auto", None)`__ を設定することにします。

### 間違った項目の設定方法

現状では、__評価値の Dropdown__ と __手番の Dropdown__ の __項目の内容は同じ__ なので、__`create_dropdown`__ では、`self.ai_dict` に __Dropdown の項目を計算__ し、__それをそのまま使って__ 下記のプログラムの 5、10 行目のように __両者の Dropdown を作成__ しています。

```python
 1  def create_dropdown(self):
略（ここで self.ai_dict の内容を計算している）
 2      for i in range(2):
略
 3          self.dropdown_list.append(
 4              widgets.Dropdown(
 5                  options=self.ai_dict,
略
 6              )
 7          ) 
 8              
 9      self.status_dropdown = widgets.Dropdown(
10          options=self.ai_dict,
略
11      )   
略
```

評価値の Dropdown に「手番の AI」という項目を追加するために、下記のプログラムのように、`create_dropdown` メソッドを修正すればよいと思う人がいるかもしれません。

* __13 行目__：手番の Dropdown を作成した後で、`self.ai_dict` の `"手番の AI"` というキーの値に `("Auto", None)` を追加する
* __15 行目__：上記の `self.ai_dict` を利用して評価値の Dropdown を作成する。この部分のプログラムには __修正を行っていない__

```python
 1  from marubatsu import Marubatsu_GUI
 2  import ipywidgets as widgets
 3  from copy import deepcopy
 4
 5  def create_dropdown(self):
元と同じなので省略
 6      for i in range(2):
元と同じなので省略
 7          self.dropdown_list.append(
 8              widgets.Dropdown(
 9                  options=self.ai_dict,
元と同じなので省略
10              )
11          ) 
12              
13      self.ai_dict["手番の AI"] = ("Auto", None)
14      self.status_dropdown = widgets.Dropdown(
15          options=self.ai_dict,
16          layout=widgets.Layout(width="100px"),
17          style={"description_width": "20px"},
18          value=select_values[0],
19      )   
20   
21  Marubatsu_GUI.create_dropdown = create_dropdown
```

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

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

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.ai_dict["手番の AI"] = ("Auto", None)
    self.ai_dict["ai1s"] = None
    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
from copy import deepcopy

def create_dropdown(self):
元と同じなので省略
    for i in range(2):
元と同じなので省略
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
元と同じなので省略
            )
        ) 
            
+   self.ai_dict["手番の AI"] = ("Auto", None)
    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>

プログラムにある程度慣れた人は、手番の Dropdown と、評価値の Dropdown が __同じ `self.ai_dict` を共有する__ ので、上記のように、`self.ai_dict` を修正すると、__手番の Dropdown にも__「手番の AI」という __項目が追加されてしまう__ と思うかもしれません。

しかし、[以前の記事](https://qiita.com/ysgeso/items/53147dbb56f83be32edc#%E6%97%A2%E3%81%AB%E4%BD%9C%E6%88%90%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B-dropdown-%E3%81%B8%E3%81%AE%E9%A0%85%E7%9B%AE%E3%81%AE%E8%BF%BD%E5%8A%A0%E3%81%AE%E6%96%B9%E6%B3%95)で説明したように、__Dropdown__ は、項目のデータを表す __`options` 属性__ の内容を __Dropdown の作成後に変更しても項目の内容が変わらない__ という性質を持つので、上記の修正を行っても、__手番の Dropdown の項目は変化しません__。

そのことは、下記のプログラムを実行することで確認できます。実行結果の左図のように、__評価値の Dropdown__ には最後に __「手番の AI」という項目が登録__ されていますが、__手番の Dropdown__ には __登録されていません__。

```python
from util import gui_play

gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b083b452-c007-f795-b016-ea74204d03c9.png" width="150">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/68447b4a-af1c-b54d-7233-01a75322b876.png" width="150">

### 問題が発生する状況とその原因

上記のように、意図したとおりに Dropdown が作成されるので、上記のプログラムで問題はないと思う人がいるかもしれませんが、実は __上記の修正には問題があります__。それは、__ファイルから Dropdown に登録されていない AI が対戦したファイルを読み込む__ と、__手番の Dropdown に「手番の AI」という項目が登録されてしまう__ という問題です。

下図は、どちらの __Dropdown にも登録されていない `ai1` VS `ai2`__ の対戦結果をファイル[^1]から読み込んだ場合の手番の Dropdown の図で、__「手番の AI」という項目__ が、ファイルを読み込んだ際に新しく登録された `ai1` と `ai2` の項目の上に __存在しています__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0767315d-3c4e-e50e-4388-231f379491cf.png" width="150">

これは、ファイルから対戦結果を読み込んだ際に、__Dropdown の項目を更新する__ ために、以下のような処理を行っているからです。なお、下記のプログラムについて忘れた方は、[以前の記事](https://qiita.com/ysgeso/items/53147dbb56f83be32edc#on_load_button_clicked-%E3%81%AE%E4%BF%AE%E6%AD%A3-1)を復習して下さい。

* __6 行目__：手番の Dropdown の `options` 属性をコピーし、`options` という変数に代入する
* __7 ~ 10 行目__：対戦結果のファイルに Dropdown に登録されていない AI が存在した場合に、`options` にその AI の項目を登録する
* __11 ~ 13 行目__：`options` を手番の Dropdown の `options` 属性に代入することで、下の Dropdown の項目を新しい `options` の内容に更新する

__6 行目__ でコピーする __手番の Dropdown__ の __`options` 属性__ は、先程説明したように __評価値の Dropdown__ の `options` 属性と __同じ dict を共有__ しているので __「手番の AI」という項目が登録されています__。そのため、上記の処理を行うと、__手番の Dropdown に「手番の AI」という項目が追加__ されてしまうことになります。

```python
 1  def on_load_button_clicked(b=None):
 2      path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
 3                                      initialdir="save")
 4      if path != "":
 5          with open(path, "rb") as f:
略
 6              options = self.dropdown_list[0].options.copy()
 7              for i in range(2):
 8                   value = (self.mb.ai[i], self.params[i]) 
 9                   if not value in options.values():
10                       options[names[i]] = value
11              for i in range(2):
12                  self.dropdown_list[i].options = options
13                  self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])            
略
```

### 問題の修正

上記の問題は、評価値の Dropdown と 手番の Dropdown が、今回の記事の修正によって __異なる項目を持つ__ ようになったにも関わらず、__両方の Dropdown の `options` 属性__ が __同じ dict を共有__ していることが原因です。

このような場合は、下記のプログラムのように、__`create_dropdown`__ で __評価値の Dropdown を作成__ する際に、__手番の Dropdown__ の項目を表す __`options` 属性の値をコピー__ したデータに対して、__「手番の AI」という項目を追加__ する必要があります。そうすることによって、評価値の Dropdown と 手番の Dropdown の __`options` 属性の値が独立する__ ことになるので、先程のような問題は発生しなくなります。

* __2 行目__：`self.ai_dict` をコピーしたデータを `self.status_ai_dict` に代入する
* __3 行目__：コピーした `self.status_ai_dict` に対して「手番の AI」の項目を追加する
* __5 行目__：コピーした `self.status_ai_dict` を使って手番のの Dropdown を作成する

```python
 1  def create_dropdown(self):
元と同じなので省略
 2      self.status_ai_dict = self.ai_dict.copy()
 3      self.status_ai_dict["手番の AI"] = ("Auto", None)
 4      self.status_dropdown = widgets.Dropdown(
 5          options=self.status_ai_dict,
 6          layout=widgets.Layout(width="100px"),
 7          style={"description_width": "20px"},
 8          value=select_values[0],
 9      )   
10   
11  Marubatsu_GUI.create_dropdown = create_dropdown
```

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

```python
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_ai_dict = self.ai_dict.copy()
    self.status_ai_dict["手番の AI"] = ("Auto", None)
    self.status_dropdown = widgets.Dropdown(
        options=self.status_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
def create_dropdown(self):
元と同じなので省略
+   self.status_ai_dict = self.ai_dict.copy()
-   self.ai_dict["手番の AI"] = ("Auto", None)
+   self.status_ai_dict["手番の AI"] = ("Auto", None)
    self.status_dropdown = widgets.Dropdown(
-       options=self.ai_dict,
+       options=self.status_ai_dict,
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )   
   
Marubatsu_GUI.create_dropdown = create_dropdown
```
</details>

:::note
一度作成した Dropdown の項目を後から変更しない場合は、上記の修正を行わなくても問題は発生しません。ただし、異なる項目を持つ複数の Dropdown の `options` 属性が同じ dict を共有しても問題がないというのは、ipywidgets の Dropdown がたまたまそのように設計されているからであり、一般的ではないと思います。

また、最初は Dropdown の項目を後から変更しないと思っていても、後から変更するような修正を行うことがあるかもしれないので、異なる項目を持つ Dropdown を作成する場合は、`options` 属性に同じ dict を共有すべきではないでしょう。
:::

上記の修正後に、下記のプログラムを実行して ai1 VS ai2 のファイルを読み込むと、実行結果の左図のように、手番の Dropdown に「手番の AI」という項目が登録されなくなります。一方で、右図のように、__評価値の Dropdown に `ai1` と `ai2` が登録されなくなる__ という問題が発生します。

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/00c1b166-fe8d-5ed8-6c81-3a3c0a2e593d.png" width="150">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/448d3ff9-5d63-ae66-3878-dae86fb67b3f.png" width="150">

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

上記の問題は、評価値と手番の Dropdown の __`options` 属性が異なるデータになったことが原因__ なので、下記のプログラムのように __`on_load_button_clicked`__ 内で、__評価値の Dropdown の項目を更新する処理__ を記述する必要があります。下記の修正で行っている処理は、__`create_dropdown` と同様__ に、__手番の Dropdown__ の項目を表す __`options` 属性をコピー__ し、__そのデータに「AI の手番」の項目を追加する__ というものです。

なお、今回の修正とは別に、__13 行目の `mb.ai[i]` が間違っていた__ 点に気が付きましたので、__`self.mb.ai[i]`__ のように修正しました。

* __17 行目__：手番の Dropdown の項目のデータを表す `options` をコピーして `status_options` に代入する
* __18、19 行目__：`status_options` に「AI の手番」の項目を追加し、評価値の Dropdown の `options` 属性に `status_options` の値を代入して更新する

```python
 1  import math
 2  import pickle
 3  from tkinter import Tk, filedialog
 4
 5  def create_event_handler(self):
元と同じなので省略
 6      # 開く、保存ボタンのイベントハンドラを定義する
 7      def on_load_button_clicked(b=None):
元と同じなので省略
 8          if path != "":
 9              with open(path, "rb") as f:
元と同じなので省略
10                  if "names" in data:
11                      names = data["names"]
12                  else:
13                      names = [ "人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__ for i in range(2)]                       
元と同じなので省略
14                  for i in range(2):
15                      self.dropdown_list[i].options = options
16                      self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])            
17                  status_options = options.copy()
18                  status_options["手番の AI"] = ("Auto", None)
19                  self.status_dropdown.options = status_options
元と同じなので省略
20
21  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

```python
import math
import pickle
from tkinter import Tk, filedialog

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 self.mb.ai[i] is None else self.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])            
                status_options = options.copy()
                status_options["手番の AI"] = ("Auto", None)
                self.status_dropdown.options = status_options
                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
import pickle
from tkinter import Tk, filedialog

def create_event_handler(self):
元と同じなので省略
    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b=None):
元と同じなので省略
        if path != "":
            with open(path, "rb") as f:
元と同じなので省略
                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)]                       
+                   names = [ "人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__ for i in range(2)]                       
元と同じなので省略
                for i in range(2):
                    self.dropdown_list[i].options = options
                    self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])            
+               status_options = options.copy()
+               status_options["手番の AI"] = ("Auto", None)
+               self.status_dropdown.options = status_options
元と同じなので省略

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

上記の修正後に、下記のプログラムを実行して ai1 VS ai2 のファイルを読み込むと、実行結果のように、__評価値の Dropdown に `ai1` と `ai2` が登録される__ ことが確認できます。

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/24fa9037-7846-fb66-3c09-bcc5e84613b0.png" width="150">

## Marubatsu_GUI クラスの `update_gui` メソッドの修正

次に __「手番の AI」が選択されていた場合の処理__ を下記のプログラムのように __`update_gui` に記述__ する必要があります。__手番の AI の情報__ は __Marubatsu__ クラスのインスタンスの __`ai` 属性__ に、__AI のパラメータの情報__ は __Marubatsu_GUI__ クラスの __`params` 属性__ に代入されているので、それらを利用します。

なお、__ゲーム盤のマスに評価値の値の表示が収まらな_場合がある__ ことがわかったので、17 行目で __評価値のフォントサイズ__ を 5 から 4.5 に __少し小さくしました__。

* __7 ~ 10 行目__：`ai` の値が `"Auto"` の場合は、8 行目で現在の手番を表すインデックスを計算し、9、10 行目で手番の AI と パラメーターを計算して `ai` と `params` に代入する

```python
 1  from marubatsu import Marubatsu
 2
 3  def update_gui(self):
元と同じなので省略
 4      if self.show_status:
 5          bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
 6          ai, params = self.status_dropdown.value
 7          if ai == "Auto":
 8              index = 0 if self.mb.turn == Marubatsu.CIRCLE else 1
 9              ai = self.mb.ai[index]
10              params = self.params[index]
11          if ai is not None:
12              analyze = ai(self.mb, analyze=True, **params)
13              score_by_move = analyze["score_by_move"]
14              candidate = analyze["candidate"]
元と同じなので省略
15                  if score_by_move is not None:
16                      color = "red" if move in candidate else "black"
17                      ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=4.5*self.size, c=color)
元と同じなので省略
18        
19  Marubatsu_GUI.update_gui = update_gui
```

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

```python
from marubatsu import Marubatsu

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 == "Auto":
            index = 0 if self.mb.turn == Marubatsu.CIRCLE else 1
            ai = self.mb.ai[index]
            params = self.params[index]
        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

def update_gui(self):
元と同じなので省略
    if self.show_status:
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
        ai, params = self.status_dropdown.value
+       if ai == "Auto":
+           index = 0 if self.mb.turn == Marubatsu.CIRCLE else 1
+           ai = self.mb.ai[index]
+           params = self.params[index]
        if ai is not None:
            analyze = ai(self.mb, analyze=True, **params)
            score_by_move = analyze["score_by_move"]
            candidate = analyze["candidate"]
元と同じなので省略
                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)
+                   ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=4.5*self.size, c=color)
元と同じなので省略

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

上記の修正後に、下記のプログラムを実行して下記の処理を行うことで、正しく処理が行われるかどうかを確認します。

1. `ai1s` VS `ai14s` で対戦を行う
2. 評価値の Dropdown に「手番の AI」を選択し、「状況」ボタンをクリックする
3. リプレイボタンをクリックしてゲーム開始時の局面と 1 手目の局面を表示する

実行結果の左図のように、__ゲーム開始時の局面__ では __`ai1s` が計算した評価値__ が、右図の __1 手目の局面__ では、__`ai14s` が計算した評価値が表示__ されることが確認できます。

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/75e59ee0-1439-5bc7-c7b7-525db514788f.png" width="150">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b6cc0b73-7b9f-80f1-80f4-1d493db4f429.png" width="150">

# ゲーム木を利用した AI の評価値の表示

現状では、__ゲーム木を利用__ して着手を選択する AI である __`ai_gt6`__ は、下図のように __候補手を表示__ します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3faf7137-30c7-1d7c-42fc-8a2494062acd.png" width="150">

これは、`ai_gt6` の仮引数 __`bestmoves_by_board`__ には、__評価値の情報が記録されていない__ ためです。しかし、実際には `bestmoves_by_board` の元となった、__ゲーム木の中には評価値が計算済__ なので、__ゲーム木を利用した AI__ も __評価値を表示することが可能__ です。

そのために必要となる、__[ゲーム木の局面と最善手・評価値の対応表](https://qiita.com/ysgeso/items/c4150416c994571b2803#%E5%B1%80%E9%9D%A2%E3%81%A8%E6%9C%80%E5%96%84%E6%89%8B%E8%A9%95%E4%BE%A1%E5%80%A4%E3%81%AE%E5%AF%BE%E5%BF%9C%E8%A1%A8%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E4%BD%9C%E6%88%90)__ のデータは、bestmoves_and_score_by_board.dat という __ファイルに保存済__ なので、それを利用して __`analyze=True`__ を実引数に記述して呼び出した際に、__それぞれの合法手を着手した際の評価値を計算__ して返す __`ai_gt7`__ という AI の関数を定義する事にします。どのように定義すればよいかについて少し考えてみて下さい。

## `ai_gt7` の定義の方法

`ai_gt7` は、ゲーム木の局面と最善手・評価値の対応表のデータを使って、__局面の情報から直接最善手の一覧を計算__ する処理を行います。そのため、局面の情報から評価値を計算して返す関数に対して利用できる __`ai_by_score`__ のデコレーターを __利用することはできません__。

__`ai_by_candidate`__ のデコレーターは、下記のプログラムの 5 行目のように、__評価値の一覧のデータを返すように作られてい__ ないため、`ai_by_candidate` を __利用することもできません__。

```python
1  def ai_by_candidate(func):
略
2          if analyze:
3              return {
4                  "candidate": candidate,
5                  "score_by_move": None
6              }
略
```

### `ai_gt7` と `ai_by_candidate` 修正する方法

一つの方法として、以下のように __`ai_gt7` を定義__ し、__`ai_by_candidate` を修正する__ という方法が考えられます。

* __`ai_gt7`__ が __返り値__ として、__候補手の一覧__ と、それぞれの合法手に着手を行った際の局面の __評価値の一覧__ を返すように定義する
* __`ai_by_candidate`__ を `ai_gt7` の返り値を利用して、__評価値の一覧を返すように修正__ する

しかし、この方法には大きな欠点があります。それは、上記の方法では __`ai_gt7`__ が __必ず評価値の一覧を計算する必要__ がある点です。

__`ai_gt7`__ が __着手を選択する際__ に、それぞれの合法手に着手を行った際の局面の __評価値の情報は必要ありません__。それにも関わらず、__必ずその情報を計算する__ ように `ai_gt7` を定義してしまうと、`ai_gt7` の処理が __必要のない処理を行う分だけ遅くなってしまいます__。

そのため、本記事ではこの方法は採用しません。

### デコレーターを利用しない方法

もう一つの方法は、__デコレーターを利用せず__ に `ai_gt7` が __ラッパー関数によって追加する処理を含めた__ 処理を行うようにする方法です。この方法であれば、仮引数 __`analyze` に `True` が代入されている場合のみ__、評価値の一覧を計算することができるようになります。

下記は、そのように __`ai_gt7`__ を定義したプログラムです。

* __5 行目__：仮引数に着手を選択するために必要な `mborig`、`bestmoves_and_score_by_board` と、ラッパー関数に記述していた `debug`、`rand`、`analyze` を全て持つように関数を定義する。局面のデータはコピーする場合があるので、仮引数の名前を `mborig` とした
* __6 行目__：候補手の一覧を `bestmoves_and_score_by_board` の `"bestmoves"` 属性から取り出して、`candidate` に代入する
* __7 行目__：ラッパー関数で行っていた、候補手のデバッグ表示を行う
* __8 ~ 20 行目__：`analyze` が `True` の場合は、それぞれの合法手に着手を行った場合の評価値を計算する
* __9 行目__：合法手の一覧を計算する
* __10 行目__：それぞれの合法手に着手を行った場合の局面の評価値を記録する `score_by_move` を空の dict で初期化する
* __11 ~ 15 行目__：それぞれの合法手に対する繰り返し処理を行う
* __12 ~ 14 行目__：現在の局面を表す `mborig` をコピーして `mb` に代入し、その `mb` に対して合法手の着手を行う
* __15 行目__：`mb` の局面の評価値を `bestmoves_and_score_by_board` の `"score"` 属性から取り出して、`score_by_move` に記録する
* __16 行目__：評価値の一覧のデバッグ表示を行う
* __17 ~ 20 行目__：候補手の一覧と評価値の一覧を表す dict を返す
* __21 ~ 25 行目__：`analyze` が `False` の場合の処理を行う。この部分は `ai_by_candidate` のラッパー関数で行う処理と全く同じである

```python
 1  from ai import dprint
 2  from copy import deepcopy
 3  from random import choice
 4
 5  def ai_gt7(mborig, debug=False, bestmoves_and_score_by_board=None, rand=True, analyze=False):
 6      candidate = bestmoves_and_score_by_board[mborig.board_to_str()]["bestmoves"]
 7      dprint(debug, "candidate", candidate)
 8      if analyze:
 9          legal_moves = mborig.calc_legal_moves()
10          score_by_move = {}
11          for move in legal_moves:
12              mb = deepcopy(mborig)
13              x, y = move
14              mb.move(x, y)
15              score_by_move[move] = bestmoves_and_score_by_board[mb.board_to_str()]["score"]
16          dprint(debug, "score_by_move", score_by_move) 
17          return {
18              "candidate": candidate, 
19              "score_by_move": score_by_move
20          }
21      else:
22          if rand:
23              return choice(candidate)
24          else:
25              return candidate[0]
```

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

```python
from ai import dprint
from copy import deepcopy
from random import choice

def ai_gt7(mborig, debug=False, bestmoves_and_score_by_board=None, rand=True, analyze=False):
    candidate = bestmoves_and_score_by_board[mborig.board_to_str()]["bestmoves"]
    dprint(debug, "candidate", candidate)
    if analyze:
        legal_moves = mborig.calc_legal_moves()
        score_by_move = {}
        for move in legal_moves:
            mb = deepcopy(mborig)
            x, y = move
            mb.move(x, y)
            score_by_move[move] = bestmoves_and_score_by_board[mb.board_to_str()]["score"]
        dprint(debug, "score_by_move", score_by_move)   
        return {
            "candidate": candidate, 
            "score_by_move": score_by_move
        }
    else:
        if rand:
            return choice(candidate)
        else:
            return candidate[0]
```
</details>

上記の実行後に、下記のプログラムを実行すると、実行結果のように着手が選択されることが確認できます。なお、ゲーム開始時の局面では、すべての合法手が最善手なので、表示される結果にはすべてのマスが表示される可能性があります。

```python
from util import load_bestmoves

mb = Marubatsu()
bestmoves_and_score_by_board = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
print(ai_gt7(mb, bestmoves_and_score_by_board=bestmoves_and_score_by_board))
```

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

```
(0, 0)
```

また、下記のプログラムのように、実引数に __`analyze=True`__ を記述して `ai_gt7` を呼び出すと、実行結果のように候補手の一覧と評価値の一覧が表示されることが確認できます。

```python
from pprint import pprint

pprint(ai_gt7(mb, bestmoves_and_score_by_board=bestmoves_and_score_by_board, analyze=True))
```

実行結果

```
{'candidate': [(0, 0),
               (1, 0),
               (2, 0),
               (0, 1),
               (1, 1),
               (2, 1),
               (0, 2),
               (1, 2),
               (2, 2)],
 'score_by_move': {(0, 0): 0,
                   (0, 1): 0,
                   (0, 2): 0,
                   (1, 0): 0,
                   (1, 1): 0,
                   (1, 2): 0,
                   (2, 0): 0,
                   (2, 1): 0,
                   (2, 2): 0}}
```

## `gui_play` の修正

次に、下記のプログラムのように __`gui_play`__ の __AI の項目に `ai_gt7` を利用した AI を登録__ するように修正します。

* __4 行目__：bestmoves_and_score_by_board.dat を読み込むように修正する
* __5 行目__：項目の名前を `ai_gt7` に修正し、4 行目のデータをパラメータとして利用するように修正する
* __6 行目__：bestmoves_and_score_by_board_shortest_victory.dat を読み込むように修正する
* __7 行目__：6 行目のデータをパラメータとして利用するように修正する。項目の名前は `ai_gtsv` のまま変更しない

```python
1  import ai as ai_module
2
3  def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
4          bestmoves_and_score_by_board = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
5          ai_dict["ai_gt7"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board})
6          bestmoves_and_score_by_board_sv = load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat")
7          ai_dict["ai_gtsv"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board_sv})
元と同じなので省略 
```

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

```python
import ai as ai_module

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    if params is None:
        params = [{}, {}]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": ( None, {} ) }
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = (getattr(ai_module, ai_name), {})
        bestmoves_and_score_by_board = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
        ai_dict["ai_gt7"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board})
        bestmoves_and_score_by_board_sv = load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat")
        ai_dict["ai_gtsv"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board_sv})
            
    mb = Marubatsu()
    mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)
 ```
</details>

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

```diff_python
import ai as ai_module

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
-       bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
+       bestmoves_and_score_by_board = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
-       ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
+       ai_dict["ai_gt7"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board})
-       bestmoves_by_board_sv = load_bestmoves("../data/bestmoves__by_board_shortest_victory.dat")
+       bestmoves_and_score_by_board_sv = load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat")
-       ai_dict["ai_gtsv"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board_sv})
+       ai_dict["ai_gtsv"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board_sv})
元と同じなので省略 
```
</details>

上記の修正後に、下記のプログラムを実行して (1, 1) に着手を行った後で、上の Dropdown に `ai_gt7` を選択して「状況」ボタンをクリックすると、実行結果の左図のようにゲーム盤のマスに評価値が表示されるようになったことが確認できます。なお、(1, 1) に着手を行ったのは、ゲーム開始時の局面では、`ai_gt7` と `ai_gtsv` で表示が変化しないからです。

次に `ai_gtsv` を選択すると、実行結果の右図のように、左図とは異なる評価値が表示されることが確認できます。

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/17f635b9-5587-3930-5a69-0e65f84da216.png" width="150">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/69526d76-d542-b1e8-403d-71bc690fe441.png" width="150">

本記事では図示しませんが、興味がある方は `ai_gt7` と `ai_gtsv` のそれぞれについて、__ゲーム盤に表示__ される評価値と下の __ゲーム木に表示__ される __評価値が一致する__ ことを確認してみて下さい。

# 今回の記事のまとめ

今回の記事では、評価値を表示する AI に手番の AI が自動的に選択する改良と、ゲーム木を利用した AI が評価値の一覧を計算できるようにする改良を行いました。

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

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

# 次回の記事

https://qiita.com/ysgeso/items/86bcfad601e877cfc284

[^1]:そのファイルは今回の記事の github の save フォルダ内に ai1 VS ai2 2024年09月11日 15時56分34秒.mbsav という名前で保存しておきましたので、興味がある方は試してみて下さい