Pythonで〇×ゲームのAIを一から作成する　その123　Dropdown による最善手・評価値の対応表の変更

# 目次と前回の記事

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

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

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

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

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

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

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

# Dropdown による最善手・評価値の対応表の変更

前回の記事で、Mbtree_GUI クラスのインスタンスを作成する際に、__実引数で局面と最善手・評価値の対応表を選択__ できるように修正しました。今回の記事では、Mbtree_GUI クラスのインスタンスを __作成した後で__、局面と最善手・評価値の対応表を __変更できるようにします__。

具体的には、Marubatsu_GUI クラスで AI の選択を Dropdown で行えるようにしたのと同様に、__Dropdown を使って__ 局面と最善手・評価値の対応表を __変更できるようにします__。

## Mbtree_GUI クラスの `__init__` メソッドの修正

Marubatsu_GUI クラスの `__init__` メソッドでは、下記のプログラムのように __仮引数 `ai_dict`__ に Dropdown に登録する AI の一覧を表すデータを __dict の形式で代入しました__。

```python
def __init__(self, mb, params, names, ai_dict, seed, size):
```

そこで、Mbtree_GUI クラスの `__init__` メソッドも同様に、__`scoretable_dict`__ という名前の仮引数に Dropdown に登録する __局面と最善手・評価値の対応表の一覧を表すデータを代入__ することにします。なお、仮引数の名前を `bestmoves_and_score_by_board_dict` とするのはさすがに長すぎる気がしたので、`scoretable_dict` としました。

__`scoretable_dict`__ には、Dropdown を作成する際にキーワード引数 __[`options` に代入するデータ](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#options)__ を代入します。具体的には、下記のプログラムのような、__キー__ に Dropdown に表示する __項目名__ を、__キーの値__ に __局面と最善手・評価値の対応表のデータ__ を代入します。

なお、基本となる bestmoves_and_score_by_board.dat に対応する項目名は「標準」を表す Standard としました。また、Dropdown に表示する項目名の末尾には () の中にその対応表を利用する AI の名前を記述しました。

```python
from util import load_bestmoves

scoretable_dict = {
    "Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
    "Shortest victory (ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
}
```

下記は、`__init__` メソッドを修正したプログラムです。`scoretable_dict` に対応するキーワード引数を省略できるように `None` をデフォルト値とする仮引数とし、`None` が代入されている場合は上記のデータを代入することにします。
* __4 行目__：仮引数 `bestmoves_and_score_by_board` を削除し、デフォルト値を `None` とする仮引数 `scoretable_dict` を追加する
* __5 ~ 11 行目__：`scoretable_dict` が `None` の場合に、上記のデータを `scoretable_dict` に代入する
* __6 行目__：9、10 行目の処理で `load_bestmoves` が必要になるが、mbtree.py の先頭で `load_bestmoves` を util.py からインポートすると循環参照が発生するので、[ローカルなインポート](https://qiita.com/ysgeso/items/6d1430a88e251279d77a#%E5%BE%AA%E7%92%B0%E5%8F%82%E7%85%A7%E3%81%AE%E5%95%8F%E9%A1%8C%E3%81%AE%E4%BF%AE%E6%AD%A3)で `load_bestmoves` をインポートする必要がある
* __12 行目__：`bestmoves_and_score_by_board` を同名の属性に代入する処理を削除し、代わりに`scoretable_dict` を同名の属性に代入する処理を記述する

```python
 1  from tree import Node, Mbtree_GUI
 2  from marubatsu import Marubatsu
 3
 4  def __init__(self, scoretable_dict=None, show_score=True, size=0.15):
 5      if scoretable_dict is None:
 6          from util import load_bestmoves
 7
 8          scoretable_dict = {
 9              "Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
10              "Shortest victory (ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
11          }
12      self.scoretable_dict = scoretable_dict
元と同じなので省略
13    
14  Mbtree_GUI.__init__ = __init__
```

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

```python
from tree import Node, Mbtree_GUI
from marubatsu import Marubatsu

def __init__(self, scoretable_dict=None, show_score=True, size=0.15):
    if scoretable_dict is None:
        scoretable_dict = {
            "Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
            "Shortest victory (ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
        }
    self.scoretable_dict = scoretable_dict
    self.show_score = show_score
    self.size = size
    self.width = 50
    self.height = 65
    self.selectednode = Node(Marubatsu())
    super(Mbtree_GUI, self).__init__()
    
Mbtree_GUI.__init__ = __init__
```
</details>

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

```diff_python
from tree import Node, Mbtree_GUI
from marubatsu import Marubatsu

-def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):
+def __init__(self, scoretable_dict=None, show_score=True, size=0.15):
+   if scoretable_dict is None:
+       scoretable_dict = {
+           "Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
+           "Shortest victory(ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
+       }
-   self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
+   self.scoretable_dict = scoretable_dict
    self.scoretable_dict = scoretable_dict
元と同じなので省略
    
Mbtree_GUI.__init__ = __init__
```
</details>

## Mbtree_GUI クラスの `create_widgets` メソッドの修正

次に、__`scoretable_dict`__ の値を元に、__Dropdown のウィジェットを作成__ する必要があります。下記は、そのように `create_widgets` メソッドを修正したプログラムです。

* __5 ~ 8 行目__：`scoretable_dict` 属性の値を使って Dropdown を作成する。なお、Dropdown の説明文（description）は score table とした
* __9 行目__：先程 `__init__` メソッド内で __`bestmoves_and_score_by_board` 属性に値を代入する処理を削除したが__、局面と最善手・評価値の対応表を表す __この属性は `update_gui` で部分木を描画する際に必要__ となる。Dropdown の項目の値には局面と最善手・評価値の対応表のデータが代入されているので、Dropdown で __選択されている項目の値__（value 属性）を __その属性に代入する__

```python
 1  import ipywidgets as widgets
 2  import matplotlib.pyplot as plt
 3
 4  def create_widgets(self):
元と同じなので省略
 5      self.dropdown = widgets.Dropdown(
 6          options=self.scoretable_dict,
 7          description="score table",
 8      )
 9      self.bestmoves_and_score_by_board = self.dropdown.value
10
11  Mbtree_GUI.create_widgets = create_widgets
```

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

```python
import ipywidgets as widgets
import matplotlib.pyplot as plt

def create_widgets(self):
    self.output = widgets.Output()  
    self.print_helpmessage()
    self.output.layout.display = "none"
    self.left_button = self.create_button("←", 50)
    self.up_button = self.create_button("↑", 50)
    self.right_button = self.create_button("→", 50)
    self.down_button = self.create_button("↓", 50)
    self.score_button = self.create_button("評価値の表示", 100)
    self.size_slider = widgets.FloatSlider(min=0.05, max=0.25, step=0.01, description="size", value=self.size)
    self.help_button = self.create_button("？", 50)
    self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
    
    with plt.ioff():
        self.fig = plt.figure(figsize=[self.width * self.size,
                                        self.height * self.size])
        self.ax = self.fig.add_axes([0, 0, 1, 1])
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False    
    
    self.dropdown = widgets.Dropdown(
        options=self.scoretable_dict,
        description="score table",
    )
    self.bestmoves_and_score_by_board = self.dropdown.value

Mbtree_GUI.create_widgets = create_widgets
```
</details>

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

```diff_python
import ipywidgets as widgets
import matplotlib.pyplot as plt

def create_widgets(self):
元と同じなので省略
+   self.dropdown = widgets.Dropdown(
+       options=self.scoretable_dict,
+       description="score table",
+   )
+   self.bestmoves_and_score_by_board = self.dropdown.value

Mbtree_GUI.create_widgets = create_widgets
```
</details>

## Mbtree_GUI クラスの `display_widgets` メソッドの修正

次に、作成した __Dropdown を表示__ するようにする必要があります。本記事では、下記のプログラムの 5 行目のように、部分木の大きさを変更する __IntSlider の下に Dropdown を配置して表示__ することにしました。

```python
1  def display_widgets(self):
2      hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
3      hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
4                          self.size_slider, self.help_button])
5      hbox3 = widgets.HBox([self.label, self.down_button, self.label, self.dropdown])
6      self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
7      display(self.vbox)  
8    
9  Mbtree_GUI.display_widgets = display_widgets
```

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

```python
def display_widgets(self):
    hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
    hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
                        self.size_slider, self.help_button])
    hbox3 = widgets.HBox([self.label, self.down_button, self.label, self.dropdown])
    self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
    display(self.vbox)  
    
Mbtree_GUI.display_widgets = display_widgets
```
</details>

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

```diff_python
def display_widgets(self):
    hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
    hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
                        self.size_slider, self.help_button])
-   hbox3 = widgets.HBox([self.label, self.down_button, self.label])
+   hbox3 = widgets.HBox([self.label, self.down_button, self.label, self.dropdown])
    self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
    display(self.vbox)  
    
Mbtree_GUI.display_widgets = display_widgets
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果のように 2 つの項目が登録された Dropdown が表示されるようになります。なお、__実引数 `scoretable_dict` を省略できる__ ように `__init__` メソッドを定義したので、以後は省略することにします。

```python
Mbtree_GUI(scoretable_dict)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/68247d19-0fb0-1ea8-af4f-c99ba717b1a1.png" width="300">

## Mbtree_GUI クラスの `create_event_handler` メソッドの修正

次に、__Dropdown の項目を変更した際__ に、部分木を表示する際に利用する __局面と最善手・評価値の対応表が変わる__ ようにする必要があります。そのためには、下記のプログラムのように、Dropdown の項目が変更された場合の __イベントハンドラ__ を `create_event_handler` の中に __記述する必要__ があります。

* __4 ~ 6 行目__：Dropdown を変更した際に実行するイベントハンドラを定義する
* __5 行目__：`bestmoves_and_score_by_board` 属性の値を、Dropdown で選択中の項目の値で更新する（この部分は、`changed["new"]` を代入しても良い）
* __6 行目__：`update_gui` メソッドを呼び出して、部分木の表示を更新する
* __8 行目__：Dropdown とイベントハンドラを結び付ける

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      self.help_button.on_click(on_help_button_clicked)
 3
 4      def on_dropdown_changed(changed):
 5          self.bestmoves_and_score_by_board = self.dropdown.value
 6          self.update_gui()
 7
 8      self.dropdown.observe(on_dropdown_changed, names="value")
 9
10      def on_key_press(event):
元と同じなので省略
11
12  Mbtree_GUI.create_event_handler = create_event_handler
```

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

```python
def create_event_handler(self):
    def on_left_button_clicked(b=None):
        if self.selectednode.parent is not None:
            self.selectednode = self.selectednode.parent
            self.update_gui()
            
    def on_right_button_clicked(b=None):
        if len(self.selectednode.children) > 0:
            self.selectednode = self.selectednode.children[0]
            self.update_gui()

    def on_up_button_clicked(b=None):
        if self.selectednode.parent is not None:
            index = self.selectednode.parent.children.index(self.selectednode)
            if index > 0:
                self.selectednode = self.selectednode.parent.children[index - 1]
                self.update_gui()
            
    def on_down_button_clicked(b=None):
        if self.selectednode.parent is not None:
            index = self.selectednode.parent.children.index(self.selectednode)
            if self.selectednode.parent.children[-1] is not self.selectednode:
                self.selectednode = self.selectednode.parent.children[index + 1]
                self.update_gui()            
                
    def on_score_button_clicked(b=None):
        self.show_score = not self.show_score
        self.update_gui()
                
    def on_size_slider_changed(changed):
        self.size = changed["new"]
        self.fig.set_figwidth(self.width * self.size)
        self.fig.set_figheight(self.height * self.size)
        self.update_gui()
                
    def on_help_button_clicked(b=None):
        self.output.layout.display = "none" if self.output.layout.display is None else None
        self.update_gui()
                        
    self.left_button.on_click(on_left_button_clicked)
    self.right_button.on_click(on_right_button_clicked)
    self.up_button.on_click(on_up_button_clicked)
    self.down_button.on_click(on_down_button_clicked)
    self.score_button.on_click(on_score_button_clicked)
    self.size_slider.observe(on_size_slider_changed, names="value")
    self.help_button.on_click(on_help_button_clicked)

    def on_dropdown_changed(changed):
        self.bestmoves_and_score_by_board = self.dropdown.value
        self.update_gui()

    self.dropdown.observe(on_dropdown_changed, names="value")

    def on_key_press(event):
        keymap = {
            "left": on_left_button_clicked,
            "0": on_left_button_clicked,
            "right": on_right_button_clicked,
            "up": on_up_button_clicked,
            "down": on_down_button_clicked,
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.selectednode.children_by_move:
                    self.selectednode = self.selectednode.children_by_move[move]
                    self.update_gui()
            except:
                pass            
            
    def on_mouse_down(event):
        for rect, node in self.mbtree.nodes_by_rect.items():
            if rect.is_inside(event.xdata, event.ydata):
                self.selectednode = node
                self.update_gui()
                break               
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)

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

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

```diff_python
def create_event_handler(self):
元と同じなので省略
    self.help_button.on_click(on_help_button_clicked)

+   def on_dropdown_changed(changed):
+       self.bestmoves_and_score_by_board = self.dropdown.value
+       self.update_gui()

+   self.dropdown.observe(on_dropdown_changed, names="value")

    def on_key_press(event):
元と同じなので省略

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

上記の修正後に下記のプログラムを実行し、(0, 0) に着手を行った局面を選択した後で Dropdown で項目を変更すると、実行結果の左図のように部分木の評価値の表示が変更されます。Dropdown の項目を変更する前の右図と見比べて下さい。

```python
Mbtree_GUI()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1141de46-51da-f8a9-f7d8-2ce2b5ce2c97.png" width="300">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1478f5c5-81a2-f066-f8dd-acffb555bf3b.png" width="300">

これで、Mbtree_GUI クラスのインスタンスを作成した後で、局面と最善手・評価値の対応表をいつでも変更できるようになりました。

# Marubatsu クラスの修正

Marubatsu_GUI クラスを修正した結果、下記のプログラムで __`gui_play`__ で対戦を行おうとすると実行結果のような __エラーが発生__ するようになります。

```python
from util import gui_play

gui_play()
```

実行結果

```
略
File c:\Users\ys\ai\marubatsu\123\marubatsu.py:604, in Marubatsu_GUI.__init__(self, mb, params, names, ai_dict, seed, size)
    602 if Marubatsu_GUI.mbtree is None:
    603     Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
--> 604 self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
略
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\site-packages\ipywidgets\widgets\widget_selection.py:121, in _make_options(x)
    118     x = x.items()
    120 # only iterate once through the options.
--> 121 xlist = tuple(x)
    123 # Check if x is an iterable of (label, value) pairs
    124 if all((isinstance(i, (list, tuple)) and len(i) == 2) for i in xlist):

TypeError: 'Mbtree' object is not iterable
```

最後のエラーメッセージは、__`widget_selection.py`__ という、__ipywidgets モジュールの中__ で記述されている __プログラムで発生__ しています。このプログラムは自分で記述したものではなく、エラーメッセージを見ても __エラーの原因がよくわかりません__。そこで、エラーメッセージをさかのぼっていくと、__Marubatsu_GUI__ クラスの __`__init__`__ メソッド内の __`self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)`__ というプログラムを実行した結果エラーが発生していることが確認できます。

先程 __Marubatsu_GUI__ クラスの __`__init__`__ メソッドの __最初の仮引数__ を、局面と最善手・評価値の __対応表__ を代入する `bestmoves_and_score_by_board` __から__、Dropdown を登録する際に必要となる局面と最善手・評価値の __対応表の一覧__ を代入する __`scoretable_dict` に変更__ しましたが、上記の Marubatsu_GUI クラスの `__init__` メソッドの処理では __その変更が反映されていない__ ことがこのエラーが発生する原因です。

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

まず、Marubatsu_GUI クラスの `__init__` メソッドを下記のプログラムのように修正する必要があります。

* __5 行目__：仮引数 `scoretable_dict` を追加する。なお、Marubatsu_GUI クラスのインスタンスは Marubatsu クラスの `play` メソッド内でのみ作成され、その際に必ず `scoretable_dict` に対応する実引数を記述して呼び出すことにするので、デフォルト引数にする必要はない
* __8 行目__：`Mbtree` はインポートする必要がなくなったのでインポートしないようにした
* 10 行目に記述されていた、ゲーム木のデータをファイルから読み込んでクラス属性 `mbtree` に代入する処理は削除した。その理由はこの後で説明する
* __10 行目__：最初の実引数を `scoretable_dict` に修正する

```python
 1  from marubatsu import Marubatsu_GUI
 2  from tkinter import Tk
 3  import os
 4
 5  def __init__(self, mb, params, names, ai_dict, scoretable_dict, seed, size):
元と同じなので省略
 6      super(Marubatsu_GUI, self).__init__()
 7    
 8      from tree import Mbtree_GUI   
 9
10      self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
11    
12  Marubatsu_GUI.__init__ = __init__               
```

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

```python
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os

def __init__(self, mb, params, names, ai_dict, scoretable_dict, seed, size):
    if params is None:
        params = [{}, {}]
    if ai_dict is None:
        ai_dict = {}
    if names is None:
        names = [None, None]
    for i in range(2):
        if names[i] is None:
            if mb.ai[i] is None:
                names[i] = "人間"
            else:
                names[i] = mb.ai[i].__name__
    
    # JupyterLab からファイルダイアログを開く際に必要な前処理
    root = Tk()
    root.withdraw()
    root.call('wm', 'attributes', '.', '-topmost', True)  

    # save フォルダが存在しない場合は作成する
    if not os.path.exists("save"):
        os.mkdir("save")        
    
    self.mb = mb
    self.ai_dict = ai_dict
    self.params = params
    self.names = names
    self.seed = seed
    self.size = size
    
    super(Marubatsu_GUI, self).__init__()
    
    from tree import Mbtree_GUI
        
    self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
    
Marubatsu_GUI.__init__ = __init__
```
</details>

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

```diff_python
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os

-def __init__(self, mb, params, names, ai_dict, seed, size):
+def __init__(self, mb, params, names, ai_dict, scoretable_dict, seed, size):
元と同じなので省略
    super(Marubatsu_GUI, self).__init__()
    
-   from tree import Mbtree, Mbtree_GUI
+   from tree import Mbtree_GUI
        
-   if Marubatsu_GUI.mbtree is None:
-       Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
-   self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
+   self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
    
Marubatsu_GUI.__init__ = __init__               
```
</details>

修正前のプログラムでは、`__init__` メソッド内で __〇×ゲームのゲーム木のデータをファイルから読み込む必要__ があり、その __所要時間が長い__ ので、同じデータを何度もファイルから読み込まなくても済むように __クラス属性 `mbtree` に読み込んだデータを代入するという工夫を行っていました__。一方、局面と最善手・評価値の対応表の __データは小さく__、ファイルからそのデータを __ほぼ一瞬で読み込むことができる__ のでそのような工夫を行う必要はありません。そのため、8 行目に記述されていたその処理を削除しました。

また、クラス属性 `mbtree` はもう必要が無くなったので、marubatsu.py の Marubatsu_GUI クラスの定義の中の下記の 2 行目のプログラムは削除します。

```python
class Marubatsu_GUI:
    mbtree = None
略
```

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

__Marubatsu_GUI__ クラスの __`mbtree` 属性が無くなった__ ので、Marubatsu_GUI クラスの中で、__`mbtree` 属性を利用するプログラムを修正する必要__ があります。

`mbtree` 属性は、__ゲーム盤に着手を行った際に__ ゲーム盤の表示を更新するために __呼び出される `update_gui`__ メソッドの下記の部分で利用されています

```python
def update_gui(self):
略
    if hasattr(self, "mbtree_gui"):
        key = tuple(self.mb.records[:self.mb.move_count + 1])
        self.mbtree_gui.selectednode = self.mbtree.nodelist_by_mb[key]
        self.mbtree_gui.update_gui()
```

上記のプログラムでは、下記の手順でゲーム盤の下に表示する GUI の部分木の __選択されたノードを現在の局面を表すノードに変更__ し、表示を更新するという処理を行います。

* ゲーム木を表す `self.mbtree` の中から、現在の局面を表すノードを探す
* __そのノードを__ Mbtree_GUI クラスのインスタンスの __`selectednode` 属性に代入__ する
* GUI の部分木の表示を更新する

`mbtree` 属性がなくなったので、__この処理を `self.mbtree` 利用せずに行う__ 必要があります。[前回の記事](https://qiita.com/ysgeso/items/c4150416c994571b2803#__init__-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%86%85%E3%81%A7-selectenode-%E5%B1%9E%E6%80%A7%E3%81%AB%E4%BB%A3%E5%85%A5%E3%81%99%E3%82%8B%E5%80%A4)で、`selectednode` 属性に代入する __ノード__ のデータには、__下記の属性のデータのみが必要__ であることがわかっています。

* ノードの深さを表す __`depth` 属性__
* 親ノードを表す __`parent` 属性__

上記のうち、__ノードの深さ__ は __`self.mb.move_count`__ によって計算できますが、局面を表す `self.mb` のデータから、__親ノードを計算するのは少々面倒__ です。

__親ノードのデータ__ は、下記のプログラムの Mbtree_GUI クラスの `update_gui` メソッドの中で、__深さが 6 より大きい場合__ に中心となるノードを計算するために __必要__ なので、__深さが 6 以下__ のノードの場合は __必要がありません__。

```python
def update_gui(self):
略
     centernode = self.selectednode
     while centernode.depth > 6:
         centernode = centernode.parent
略
```

そこで、下記のプログラムのように、__親ノードを計算する処理は一旦保留__ し、__親ノードを持たない__ `self.mb` の __ノードを作成__ して `selectednode` 属性に代入することにします。

* __5 行目__：7 行目の処理で Node クラスが必要になるが、marubatsu.py の先頭で Node クラスを mbtree.py からインポートすると循環参照が発生するので、[ローカルなインポート](https://qiita.com/ysgeso/items/6d1430a88e251279d77a#%E5%BE%AA%E7%92%B0%E5%8F%82%E7%85%A7%E3%81%AE%E5%95%8F%E9%A1%8C%E3%81%AE%E4%BF%AE%E6%AD%A3)で Node クラスをインポートする必要がある
* __7 行目__：現在の局面を表すノードを作成し、`selectednode` 属性に代入する。ただし、現時点では親ノードは存在しないものとしてノードを作成する
* __8 行目__：部分木の表示を更新する

```python
 1  def update_gui(self):
元と同じなので省略
 2      self.update_widgets_status()
 3      
 4      if hasattr(self, "mbtree_gui"):
 5          from tree import Node
 6
 7          self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
 8          self.mbtree_gui.update_gui()
 9        
10  Marubatsu_GUI.update_gui = update_gui
```

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

```python
def update_gui(self):
    ax = self.ax
    ai = self.mb.ai
    
    # 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=20, ha="center")   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(0, -0.2, text, fontsize=20)
    
    self.draw_board(ax, self.mb)
    
    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
def update_gui(self):
元と同じなので省略
    self.update_widgets_status()
    
    if hasattr(self, "mbtree_gui"):
+       from tree import Node

-       key = tuple(self.mb.records[:self.mb.move_count + 1])
-       self.mbtree_gui.selectednode = self.mbtree.nodelist_by_mb[key]
+       self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
+       self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

## Marubatsu クラスの `play` メソッドの修正

次に、__Marubatsu_GUI__ クラスの __インスタンスを作成__ する処理を行う __`play`__ メソッドを下記のプログラムのように修正する必要があります。

* __3 行目__：デフォルト値を `None` とする仮引数 `scoretable_dict` を追加する
* __7 行目__：Marubatsu_GUI クラスのインスタンスを作成する際に、キーワード引数　`scoretable_dict=scoretable_dict` を記述するように修正する

```python
1  from random import random
2
3  def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
4      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
5      if gui:
6          mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict,
7                                 scoretable_dict=scoretable_dict, seed=seed, size=size)
元と同じなので省略
8
9   Marubatsu.play = play
```

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

```python
from random import random

def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None, verbose=True, seed=None, gui=False, size=3):
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
        
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
    self.verbose = verbose
    self.gui = gui
    
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict,
                               scoretable_dict=scoretable_dict, seed=seed, size=size)
    else:
        mb_gui = None
        
    self.restart()
    return self.play_loop(mb_gui, params=params)

Marubatsu.play = play
```
</details>

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

```diff_python
from random import random

-def play(self, ai, ai_dict=None, params=None, names=None, verbose=True, seed=None, gui=False, size=3):
+def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict,
-                              seed=seed, size=size)  
+                              scoretable_dict=scoretable_dict, seed=seed, size=size)  
元と同じなので省略

Marubatsu.play = play
```
</details>

上記の修正後に下記のプログラムを実行して、ゲーム盤に着手を行ってください。__6 手目まで__ はゲーム盤の下の __GUI の部分木が正しく更新__ されますが、__7 手目の着手を行うと__ 実行結果のような __エラーが発生__ します。

:::note
`gui_play` の中ではキーワード引数 `scoretable_dict` を記述せずに Marubatsu クラスのインスタンスの `play` メソッドを呼び出しているので、仮引数 `scoretable_dict` にはデフォルト値である `None` が代入されます。そのため、その後の処理で Mbtree_GUI クラスのインスタンスを作成する際にはキーワード引数 `scoretable_dict` には `None` が代入されることになり、今回の記事で修正した Mbtree_GUI クラスの [`__init__` メソッドの最初の処理](#mbtree_gui-クラスの-__init__-メソッドの修正)によって GUI の部分木には 2 つの項目を持つ Dropdown が作成されます。
:::

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1c470370-dd33-476c-ec6d-cb41e15c4712.png" width="200">

```
略
File c:\Users\ys\ai\marubatsu\123\tree.py:909, in Mbtree_GUI.update_gui(self)
    907     maxdepth = 9
    908 centernode = self.selectednode
--> 909 while centernode.depth > 6:
    910     centernode = centernode.parent
    911 self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
    912                     "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})

AttributeError: 'NoneType' object has no attribute 'depth'
```

### エラーの検証と修正方法

エラーメッセージから、__Mbtree_GUI__ クラスの __`update_gui`__ メソッド内で、__`while centernode.depth > 6:`__ を実行した際に、__`centernode` に `None` が代入されている__ ことがわかります。先ほど説明したように、__`selectednode`__ 属性に代入する __ノードを作成する際に親ノードを作成しなかった__ ので、__`parent` 属性には `None` が代入__ されます。そのため、深さが 7 以上のノードが `centernode` 属性に代入された場合は、while 文のブロックの __`centernode = centernode.parent` が実行__ されて __`centernode` に `None` が代入__ され、次の while 文の条件式が計算されると上記のエラーが発生します。

この問題を解決する方法としては、以下の 2 種類の方法が考えられます。

* `selectednode` 属性に代入するノードを計算する際に、その __親ノードを作成する__
* 上記の `update_gui` で、__親ノードの情報を使わずに__ 中心となるノードを計算するように修正する

__後者のほうがプログラムの記述が簡単__ なので、本記事では後者の方法で修正することにしますが、前者の方法が良いと思った方は実際に実装してみて下さい。

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

`update_gui` メソッドでは、親ノードを辿ることで深さが 6 のノードを計算していますが、別の方法として、下記のプログラムのように __`records` 属性に代入された棋譜のデータを利用__ して、__ゲーム開始時の局面から 6 手目までの着手を行う__ ことで、中心となるノードの局面を計算することができます。

* __2、3 行目__：選択されたノードの深さが 6 以下の場合は、中心となるノードの局面を、選択されたノードの局面とする
* __4 ~ 7 行目__：深さが 7 以上の場合は、5 行目でゲーム開始時の局面を表すデータを作成して `centermb` に代入し、選択された局面の棋譜のデータを利用して 6 手目までの着手を行い、その局面を中心となるノードの局面とする
* __8 行目__：中心となる局面のデータを `centernode.mb` から `centermb` に修正する

```python
 1  def update_gui(self):
元と同じなので省略
 2      if self.selectednode.depth <= 6:
 3          centermb = self.selectednode.mb
 4      else:
 5          centermb = Marubatsu()
 6          for x, y in self.selectednode.mb.records[1:7]:
 7              centermb.move(x, y)
 8      self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
 9                           "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
元と同じなので省略
10
11  Mbtree_GUI.update_gui = update_gui
```

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

```python
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(-1, self.height - 1)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    if self.selectednode.depth <= 4:
        maxdepth = self.selectednode.depth + 1
    elif self.selectednode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    if self.selectednode.depth <= 6:
        centermb = self.selectednode.mb
    else:
        centermb = Marubatsu()
        for x, y in self.selectednode.mb.records[1:7]:
            centermb.move(x, y)
    self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
                          "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
    self.selectednode = self.mbtree.selectednode
    self.mbtree.draw_subtree(centernode=self.mbtree.centernode, selectednode=self.selectednode,
                            show_bestmove=True, show_score=self.show_score,
                            ax=self.ax, maxdepth=maxdepth, size=self.size)
    
    disabled = self.selectednode.parent is None
    self.set_button_status(self.left_button, disabled=disabled)
    disabled = self.selectednode.depth >= 6 or len(self.selectednode.children) == 0
    self.set_button_status(self.right_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children.index(self.selectednode) == 0
    self.set_button_status(self.up_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children[-1] is self.selectednode
    self.set_button_status(self.down_button, disabled=disabled)
    self.set_button_color(self.score_button, value=self.show_score)
    
Mbtree_GUI.update_gui = update_gui
```
</details>

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

```diff_python
def update_gui(self):
元と同じなので省略
-   centernode = self.selectednode
-   while centernode.depth > 6:
-       centernode = centernode.parent
+   if self.selectednode.depth <= 6:
+       centermb = self.selectednode.mb
+   else:
+       centermb = Marubatsu()
+       for x, y in self.selectednode.mb.records[1:7]:
+           centermb.move(x, y)
-   self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
+   self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
                         "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
元と同じなので省略

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

上記の修正後に下記のプログラムを実行して 9 手目までの着手を行うと、実行結果のようにエラーが発生しなくなったことが確認できます。

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0dd0dba4-34f0-8ecd-39b1-dde7c74b7429.png" width="350">

上記の実行結果に対して __様々な操作を行うと__、上記のプログラムには __リプレイ機能を用いるとエラーが発生する__ という問題があることがわかります。その問題については次回の記事で修正することするので、余裕がある方はそのエラーの原因と修正方法について考えておいてください。

# 今回の記事のまとめ

今回の記事では、Mbtree_GUI クラスのインスタンスを作成した後で、__局面と最善手・評価値の対応表__ を __Dropdown で変更できるよう修正__ しました。また、それにあわせて __Marubatsu クラスの `play` メソッドが動作する__ ように修正しました。

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

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

# 次回の記事

https://qiita.com/ysgeso/items/2c4dcf4a73f08750811f