Pythonで〇×ゲームのAIを一から作成する　その117　GUI の部分木への最善手の表示と最善手を着手し続けた場合の局面の表示

# 目次と前回の記事

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

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

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

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

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

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

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

# 循環参照の問題の修正

前回の記事で修正したプログラムを marubatsu.py と tree.py に反映させた後で、下記のプログラムを実行すると、実行結果のように __[循環インポート](https://qiita.com/ysgeso/items/64de676735836b963712#%E5%BE%AA%E7%92%B0%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88%E3%81%AE%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%AE%E6%84%8F%E5%91%B3%E3%81%A8%E3%81%9D%E3%81%AE%E5%AF%BE%E5%87%A6%E6%96%B9%E6%B3%95)__（circular import）の __エラーが発生__ します。また、エラーメッセージから、__`from util import gui_play`__ を実行した際にこのエラーが発生したことが確認できます。

なお、今回の記事の github の marubatsu.py は、__このエラーが発生しないように__ 原因となる __`from tree import Mbtree, Mbtree_GUI` をコメントにしています__。このエラーを確認したい人はそのコメントを外してから下記のプログラムを実行して下さい。

```python
from util import gui_play

gui_play()
```

実行結果

```
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[1], line 1
----> 1 from util import gui_play
      3 gui_play()
略
File c:\Users\ys\ai\marubatsu\117\tree.py:4
      2 import matplotlib.pyplot as plt
      3 import matplotlib.patches as patches
----> 4 from marubatsu import Marubatsu, Marubatsu_GUI
      5 from gui import GUI
      6 import ipywidgets as widgets

ImportError: cannot import name 'Marubatsu' from partially initialized module 'marubatsu' (most likely due to a circular import) (c:\Users\ys\ai\marubatsu\117\marubatsu.py)
```

循環参照が起きる原因は以下の通りです。

* [前回の記事](https://qiita.com/ysgeso/items/b5e388878fef195d8f2c#marubatsu_gui-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE-__init__-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E4%BF%AE%E6%AD%A3)で Marubatsu_GUI クラスの `__init__` メソッド内で、mbtree.py で定義された Mbtree と Mbtree_GUI クラスを利用することになったので、__marubatsu.py__ の先頭で __`from tree import Mbtree, Mbtree_GUI` を実行__ してそれらのクラスを mbtree モジュールからインポートするように修正した
* __util.py__ では、先頭の行の __`from marubatsu import Marubatsu`__ で marubatsu モジュールから Marubatsu クラスがインポートされているので __`marubatsu.py` が実行__ される
* __marubatsu.py__ で __`from tree import Mbtree, Mbtree_GUI`__ が実行された結果、__tree.py が実行される__
* __tree.py__ には __`from marubatsu import Marubatsu, Marubatsu_GUI`__ が記述されているので、__marubatsu.py が再び実行__ され、__循環インポートが発生する__

marubatu.py と mbtree.py を一つのファイルに統合することで循環インポートが発生しないようにすることができますが、それでは __〇×ゲーム__ と、__ゲーム木の処理__ を別のファイルで __分けて記述することができなくなってしまいます__。

__ファイルを統合せずに__ この問題を __解決するの方法の一つ__ に、下記のプログラムのように Marubatsu_GUI クラスの __`__init__` メソッドの中__ で __Mbtree と Mbtree_GUI クラスをインポートする__ という方法があります。

* __6 行目__：`__init__` メソッドの中で Mbtree と Mbtree_GUI クラスをインポートする

```python
 1  from marubatsu import Marubatsu_GUI
 2  from tkinter import Tk, filedialog
 3  import os
 4
 5  def __init__(self, mb, params, names, ai_dict, seed, size):
元と同じなので省略
 6      from tree import Mbtree, Mbtree_GUI
 7      if Marubatsu_GUI.mbtree is None:
 8          Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
 9      self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
10    
11  Marubatsu_GUI.__init__ = __init__
```

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

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

def __init__(self, mb, params, names, ai_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, 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)
    
Marubatsu_GUI.__init__ = __init__
```
</details>

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

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

def __init__(self, mb, params, names, ai_dict, seed, size):
元と同じなので省略
    from tree import Mbtree, 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)
    
Marubatsu_GUI.__init__ = __init__
```
</details>

この方法で循環インポートが起きなくなる理由は以下の通りです。

* __関数やメソッドの中に記述したモジュールのインポート__ は、その関数やメソッドが __呼び出されるまでは行われない__
* Marubatsu_GUI クラスの __`__init__` メソッドが呼び出される__ のは、__`from util import gui_play` を実行した後__ なので、__`from util import gui_play`__ を実行した際に __循環インポートは発生しない__
* Marubatsu_GUI クラスの __`__init__`__ メソッドが実行され、その中で __`from tree import Mbtree, Mbtree_GUI` を実行した際__ には、__既にそれらのモジュールインポートされている__ ので、__循環インポートにはならない__

このような __関数やメソッドのブロックの中に記述されたインポート__ のことを、__ローカルなインポート__ と呼びます。ローカルなインポートは __ローカル名前空間によって管理される__ ので、ローカル変数と同様に、その関数やメソッドの __ブロックの中でしか利用できない__ という性質があります。

実行結果は省略しますが、上記の修正を行った後で下記のプログラムを実行すると、エラーが発生しなくなったことが確認できます。なお、marubatsu.py の先頭の `from tree import Mbtree, Mbtree_GUI` のコメントを外した方は、元に戻した後で JupyterLab を再起動してからから下記のプログラムを実行して下さい。

```python
gui_play()
```

# 最善手の表示

今回の記事では、[前回の記事](https://qiita.com/ysgeso/items/b5e388878fef195d8f2c#%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E6%94%B9%E8%89%AF)で開始した Mbtree_GUI クラスの改良の続きを行います。

現状の __GUI の部分木[^1]の表示__ では、色によって局面の評価値がわかるようになっていますが、〇 の手番と × の手番によって評価値が大きいほうが最善手であるか、小さいほうが最善手であるかが変わるので、__どの合法手が最善手であるかが直観的にわかりにくい__ という問題があります。そこで、__最善手が着手されていない局面を暗い色で表示する__ ようにすることで、__どの合法手が最善手であるか__ を一目でわかるようにすることにします。

まず、それぞれの局面が、__最善手を着手した局面であるかを判定する__ 必要があります。どのように判定すればよいかについて少し考えてみて下さい。

## 最善手を着手した局面であるかの判定方法
__aidata.mbtree__ に保存されたゲーム木のデータには、それぞれのノードの __`bestmoves` 属性に最善手の一覧のデータが記録__ されています。また、__直前に行われた着手__ は、ノードの `mb` 属性の __`lastmove` 属性に記録__ されています。そのため、下記の手順でそれぞれのノードの局面が最善手を着手した局面であるかを判定することができます。なお、着手が行われていない、__ゲーム開始時の局面__ であるルートノードは __例外として最善手を着手した局面であると判定する__ ことにします。

1. __親ノードが存在しない場合__ はルートノードなので __最善手を着手した__ と判定する
2. __`lastmove`__ 属性が、__親ノードの `bestmoves`__ 属性の要素に __含まれている場合__ は __最善手を着手した__ と判定する
3. __上記のいずれでもない__ 場合は __最善手を着手していない__ と判定する

ゲーム木の __ノードの描画__ は Node クラスの __`draw_node`__ メソッド内で行われるので、下記の処理を記述することで最善手を着手していない局面を暗く表示することができます。

1. __ノードを描画__ する処理を __行った後__ で、__最善手を着手した局面であるかを判定__ する
2. 最善手を着手した局面でないと判定された場合は、[以前の記事](https://qiita.com/ysgeso/items/10449c1d8e1465de1ed1#%E5%8D%8A%E9%80%8F%E6%98%8E%E3%81%AE%E9%BB%92%E3%81%84%E5%9B%B3%E5%BD%A2%E3%82%92%E9%87%8D%E3%81%AD%E3%82%8B%E6%96%B9%E6%B3%95)で説明した方法で、その上に __半透明な黒い正方形の画像を描画する__ ことでその局面を __暗く表示する__

## `draw_board` の修正

[以前の記事](https://qiita.com/ysgeso/items/10449c1d8e1465de1ed1#%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E6%9A%97%E3%81%8F%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%E5%87%A6%E7%90%86%E3%81%AE%E5%AE%9F%E8%A3%85)でノードを暗く表示する処理を実装する際に、「`draw_board` を修正するということを続けていくと、draw_board の定義が複雑になるという問題が発生する」という理由でノードを暗く表示する処理を、`draw_node` とは別に実装しました。

その時はノードを暗くする処理を他の場所で行うことはないだろうと思っていたのですが、今回再び __ノードを暗く表示する処理を新たに記述する__ ことになり、そのような __同じ処理を何度も記述するのは良くないと思い直しました__ ので、今回の記事では __`draw_node` 内__ でゲーム盤を暗く表示する __処理を記述する__ ように修正することにします。

今回の記事では行いませんが、今後ゲーム盤の __外枠__ を __別の色や太さ__ で __描画したくなることがあるかもしれない__ ことを考慮して、__`draw_node`__ に __下記の仮引数を追加する__ ことにします。また、これまでは仮引数 `emphasize` によって外枠を赤く表示していましたが、外枠の描画を下記の仮引数で指定できるようになるので仮引数 __`emphasize` は削除__ します。

| 仮引数 | 意味 | デフォルト値 |
|:--|:--|:-:|
| __`bc`__[^2]  | ゲーム盤の外枠の色。`None` の場合は外枠を描画しない  | `None`  |
| __`bw`__[^3]  | ゲーム盤の外枠の幅  | `1`  |
| __`darkness`__ | ゲーム盤の表示の暗さ（darkness）を表す `0` ~ `1` の数値<br>数値が大きい程暗くなる | `0` |

下記はそのように `draw_board` を修正したプログラムです。

* __5 行目__：上記の仮引数を追加し、仮引数 `emphasize` を削除する
* 途中にあった `emphasize` が `True` の場合に外枠を描画する処理を削除する
* __7 ~ 9 行目__：ゲーム盤の描画を行った後で、`darkness` が `0` より大きい場合に、ゲーム盤の上に半透明の黒い正方形を描画して暗くする
* __12 ~ 15 行目__：`bc` が `None` 出ない場合は、ゲーム盤の外枠を `bc` の色で、`bw` の太さで描画する。この処理は、ゲーム盤を暗く表示する処理を行った後で行わないと枠の色まで暗くなってしまう点に注意すること

```python
 1  from marubatsu import Marubatsu
 2  import matplotlib.patches as patches
 3
 4  @staticmethod
 5  def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2): 
元と同じなので省略
 6      # darkness 0 より大きい場合は、半透明の黒い正方形を描画して暗くする
 7      if darkness > 0:
 8          ax.add_artist(patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
 9                                          height=mb.BOARD_SIZE, fc="black", alpha=darkness))
10
11      # bc が None でない場合はその色で bw の太さで外枠を描画する
12      if bc is not None:
13          frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
14                                    height=mb.BOARD_SIZE, ec=bc, fill=False, lw=bw)
15          ax.add_patch(frame)
16        
17  Marubatsu_GUI.draw_board = draw_board
```

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

```python
from marubatsu import Marubatsu
import matplotlib.patches as patches

@staticmethod
def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2): 
    # 結果によってゲーム盤の背景色を変更する
    if show_result:
        if score is None and mb.status == Marubatsu.PLAYING:
            bgcolor = "white"
        elif score == 1 or mb.status == Marubatsu.CIRCLE:
            bgcolor = "lightcyan"
        elif score == -1 or mb.status == Marubatsu.CROSS:
            bgcolor = "lavenderblush"
        else:
            bgcolor = "lightyellow"
        rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                height=mb.BOARD_SIZE, fc=bgcolor)
        ax.add_patch(rect)
  
    # ゲーム盤の枠を描画する
    for i in range(1, mb.BOARD_SIZE):
        ax.plot([dx, dx + mb.BOARD_SIZE], [dy + i, dy + i], c="k", lw=lw) # 横方向の枠線
        ax.plot([dx + i, dx + i], [dy, dy + mb.BOARD_SIZE], c="k", lw=lw) # 縦方向の枠線

    # ゲーム盤のマークを描画する
    for y in range(mb.BOARD_SIZE):
        for x in range(mb.BOARD_SIZE):
            color = "red" if (x, y) == mb.last_move else "black"
            Marubatsu_GUI.draw_mark(ax, dx + x, dy + y, mb.board[x][y], color, lw=lw)

    # darkness 0 より大きい場合は、半透明の黒い正方形を描画して暗くする
    if darkness > 0:
        ax.add_artist(patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                        height=mb.BOARD_SIZE, fc="black", alpha=darkness))

    # bc が None でない場合はその色で bw の太さで外枠を描画する
    if bc is not None:
        frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                                  height=mb.BOARD_SIZE, ec=bc, fill=False, lw=bw)
        ax.add_patch(frame)
        
Marubatsu_GUI.draw_board = draw_board
```
</details>

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

```diff_python
from marubatsu import Marubatsu
import matplotlib.patches as patches

@staticmethod
-def draw_board(ax, mb, show_result=False, score=None, dx=0, dy=0, lw=2): 
+def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=None, dx=0, dy=0, lw=2): 
元と同じなので省略
    # emphasize が True の場合は赤色の外枠を描画する
-   if emphasize:
-       frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
-                                 height=mb.BOARD_SIZE, ec="red", fill=False, lw=lw)
-       ax.add_patch(frame)
元と同じなので省略
    # darkness 0 より大きい場合は、半透明の黒い正方形を描画して暗くする
+   if darkness > 0:
+       ax.add_artist(patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
+                                       height=mb.BOARD_SIZE, fc="black", alpha=darkness))

    # bc が None でない場合はその色で bw の太さで外枠を描画する
+   if bc is not None:
+       frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
+                                 height=mb.BOARD_SIZE, ec=bc, fill=False, lw=bw)
+       ax.add_patch(frame)
        
Marubatsu_GUI.draw_board = draw_board
```
</details>

上記の修正後に、下記のプログラムで `draw_board` を新しい仮引数に対応する実引数を記述せずに呼び出すと、実行結果のようにこれまで通りの描画が行われることが確認できます。

```python
import matplotlib.pyplot as plt

mb = Marubatsu()
fig, ax = plt.subplots(figsize=(1,1))
ax.axis("off")
Marubatsu_GUI.draw_board(ax, mb)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b9802fa7-6668-82ee-4598-55bab16b95aa.png" width="100">

また、下記のプログラムで外枠の色を赤に、暗さを 0.3 に設定して `draw_board` を呼び出すと、実行結果のように指定した設定で描画が行われることが確認できます。

```python
fig, ax = plt.subplots(figsize=(1,1))
ax.axis("off")
Marubatsu_GUI.draw_board(ax, mb, bc="red", darkness=0.3)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/497ffa7d-755b-baab-6dd5-387c9c676234.png" width="100">

## `draw_node` の修正

__mbtree.py__ の中で、__`draw_board` を呼び出している__ のは、__Node クラスの `draw_node`__ メソッドなので、`draw_node` を下記のプログラムのように正する必要があります。

* __3 行目__：デフォルト値を `0` とする仮引数 `darkness` を追加する。なお、ノードを強調して表示すかどうかを表す仮引数 `emphasize` はそのままで良い
* __6 行目__：`emphasis` の値によって、`draw_board` を呼び出す際のキーワード引数 `bc` の値を計算する
* __7、8 行目__：`draw_board` を呼び出す際に、キーワード引数 `bc` と `darkness` を追加する

なお、`draw_node` 内では __`maxdepth` が `None` の場合__ に子ノードを __`draw_board` を呼び出して描画__ する処理を行っていますが、そちらはゲーム木の部分木を描画する __`draw_subtree` から `draw_board` を呼び出す際には実行されない__ ので、__修正していません__。

```python
 1  from tree import Node, Rect
 2
 3  def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
 4      # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
 5      y = dy + (self.height - 3) / 2
 6      bc = "red" if emphasize else None
 7      Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
 8                               score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
元と同じなので省略
 9
10  Node.draw_node = draw_node
```

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

```python
from tree import Node, Rect

def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
    width = 8
    if ax is None:
        height = len(self.children) * 4
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")
        for childnode in self.children:
            childnode.height = 4
        self.height = height         
        
    # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
    y = dy + (self.height - 3) / 2
    bc = "red" if emphasize else None
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
                            score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
    rect = Rect(dx, y, 3, 3)
    # 子ノードが存在する場合に、エッジの線と子ノードを描画する
    if len(self.children) > 0:
        if maxdepth != self.depth:   
            ax.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="k", lw=lw)
            prevy = None
            for childnode in self.children:
                childnodey = dy + (childnode.height - 3) / 2
                if maxdepth is None:
                    Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True,
                                            score=getattr(childnode, "score", None), dx=dx+5, dy=childnodey, lw=lw)
                edgey = childnodey + 1.5
                ax.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="k", lw=lw)
                if prevy is not None:
                    ax.plot([dx + 4 , dx + 4], [prevy, edgey], c="k", lw=lw)
                prevy = edgey
                dy += childnode.height
        else:
            ax.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
            
    return rect

Node.draw_node = draw_node
```
</details>

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

```diff_python
from tree import Node, Rect

-def draw_node(self, ax=None, maxdepth=None, emphasize=False, size=0.25, lw=0.8, dx=0, dy=0):
+def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
    # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
    y = dy + (self.height - 3) / 2
+   bc = "red" if emphasize else None
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
-                            score=getattr(self, "score", None), lw=lw,
+                            score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
元と同じなので省略

Node.draw_node = draw_node
```
</details>

上記の修正後に、下記のプログラムで `draw_node` を新しい仮引数に対応する実引数を記述せずに 呼び出すと、実行結果のようにこれまで通りの描画が行われることが確認できます。

```python
from tree import Mbtree

mbtree = Mbtree.load("../data/aidata")
mbtree.root.draw_node()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4623bfc9-8473-feeb-e83b-690d9999e4bc.png" width="100">　

また、下記のプログラムで `draw_node` を実引数に `emphasize=True` と `darkness=0.3` を記述して呼び出すと、実行結果のように指定した設定で描画が行われることが確認できます。なお、上記で説明したように実引数 `maxdepth` を記述せずに `draw_node` を呼び出した際に表示される子ノードには、`emphasize` と `drakness` の設定は影響を与えません。

```python
mbtree.root.draw_node(emphasize=True, darkness=0.3)
```

実行結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d45ae244-4b52-5cf5-6f71-6c0caacf580a.png" width="100">　

また、実行結果はこれまでと同じなので省略しますが、下記のプログラムで `Marubatsu_GUI` のインスタンスを作成すると、これまでと同様に GUI でゲーム木の部分木が表示されることが確認できます。実際に確認してみて下さい。

```python
from tree import Mbtree_GUI

Mbtree_GUI(mbtree)
```

## Mbtree_Anim クラスに対応した修正の必要性

__Mbtree_GUI__ クラスによるゲーム木の部分木の表示では、__ノードを暗く表示する処理は行っていません__ が、__[Mbtree_Anim クラス](https://qiita.com/ysgeso/items/c82c854c529172f74103#mbtree_anim-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E5%AE%9A%E7%BE%A9)__ でゲーム木の生成過程や、[評価値の計算過程](https://qiita.com/ysgeso/items/94c5105c3dbebb445710#%E6%B7%B1%E3%81%95%E5%84%AA%E5%85%88%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%81%AB%E3%82%88%E3%82%8B%E8%A9%95%E4%BE%A1%E5%80%A4%E3%81%AE%E8%A8%88%E7%AE%97%E3%81%AE%E9%81%8E%E7%A8%8B%E3%81%AE%E8%A6%96%E8%A6%9A%E5%8C%96)を表示する際には、[まだ作成されていないノード](https://qiita.com/ysgeso/items/10449c1d8e1465de1ed1#%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E6%9A%97%E3%81%8F%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)や[評価値が計算されていないノード](https://qiita.com/ysgeso/items/94c5105c3dbebb445710#update_gui-%E3%81%AE%E4%BF%AE%E6%AD%A3)を __暗く表示する処理__ が下記の `update_gui` メソッド内の __5 ~ 8 行目に記述されています__。

* __5 行目__：ノードが作成された順番または、評価値が計算された順番を計算して __`index`__ に代入する
* __6 ~ 8 行目__：`index` が __表示中のアニメーションのフレーム数__ を表す __`self.play.value` より大きい場合__ にそのノードを __暗く表示する__


```python
1  def update_gui(self):
略
2      self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
3                               ax=self.ax, maxdepth=maxdepth)
4      for rect, node in self.mbtree.nodes_by_rect.items():
5          index = node.score_index if self.isscore else node.id
6          if index > self.play.value:
7              self.ax.add_artist(patches.Rectangle(xy=(rect.x, rect.y), width=rect.width,
8                                                   height=rect.height, fc="black", alpha=0.5))
lw=2))
略
```

__ゲーム盤を暗く表示する処理__ は先ほど __`draw_node` で行えるようにした__ ので、__この処理を `draw_node` で行うように修正する__ ことにします。どのように修正すれば良いかについて少し考えてみて下さい。

### `draw_subtree` の修正

上記のプログラムでは下記の手順で処理が行われます。

1. 2 行目で、`draw_subtree` を呼び出してゲーム木の部分木を描画する
2. `draw_subtree` で描画した各ノードに対して、そのノードを暗く表示するかどうかを判定し、暗く表示する必要がある場合に半透明な正方形を上から描画して暗く表示する

__手順 2 の処理を `draw_board` で行うようにする__ ためには、__`draw_subtree` 内__ で `draw_node` を呼び出す際に __そのノードを暗く表示する必要があるかどうかを判定し__、暗く表示する必要がある場合はキーワード引数 `darkness` を記述して `draw_node` を呼び出すようにする必要があります。そのためには、__`draw_subtree`__ にノードを暗く表示するかどうかを __判定するために必要なデータ__ を代入する __仮引数を追加__ する必要があります。

ノードを暗く表示するかどうかの __判定に必要なデータ__ は、先程のプログラムの 5、6 行目から __`self.isscore`__ と __`self.play.value`__ の 2 つです。そこで、それらの値を代入する __`isscore`__ と __`anim_frame`__ という仮引数を __`draw_subtree` に追加__ することにします。ただし、`anim_frame` をデフォルト値を `None` とするデフォルト引数とし、`None` が代入されている場合は暗くする処理を行わないものとします。

__`draw_subtree`__ のブロックには、__`draw_node`__ を呼び出す処理が __4 箇所__ あり、それぞれに対して仮引数 `isscore` と `anim_frame` を使って暗く表示するかどうかを判定する必要があります。__同じ処理を 4 回も記述するのは無駄__ なので、`draw_subtree` の中に __表示するノードの暗さを計算する `calc_darkness` というローカル関数を定義する__ ことにします。この関数は他の関数やメソッドから利用されることはないと思いましたので、ローカル関数としました。

下記はそのように `draw_subtree` を修正したプログラムです。

* __1 行目__：デフォルト値を `None` とする仮引数 `anim_frame` と、デフォルト値を `False` とする 仮引数 `isscore` を追加する
* __2 ~ 6 行目__：仮引数 `node` に代入されたノードを表示する暗さを計算して返り値として返すローカル関数 `calc_darkness` を定義する
* __3、4 行目__：`anim_frame` が `None` の場合は暗く表示する必要がないので、暗く表示しないことを表す `0` を返り値として返す 
* __5、6 行目__：Marubatsu_Anim クラスの `update_gui` と同じ方法でノードを暗く表示するかどうかを判定し、暗く表示する場合は `0.5` を、そうでない場合は `0` を返す
* __9 ~ 16 行目__：`draw_node` を呼び出す際に、`calc_darkness` を呼び出して表示するノードの暗さを計算し、その返り値をキーワード引数 `darkness` に記述して呼び出すようにする

```python
 1  def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, size=0.25, lw=0.8, maxdepth=2):
 2      def calc_darkness(node):
 3          if anim_frame is None:
 4              return 0
 5          index = node.score_index if isscore else node.id
 6          return 0.5 if index > anim_frame else 0
 7   
 8      self.nodes_by_rect = {}
元と同じなので省略
 9                  darkness = calc_darkness(node)
10                  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略

11                  darkness = calc_darkness(sibling)
12                  rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
元と同じなので省略
13      darkness = calc_darkness(parent)
14      rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
15              darkness = calc_darkness(node)
16              rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
17              self.nodes_by_rect[rect] = node
18            
19  Mbtree.draw_subtree = draw_subtree
```

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

```python
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                darkness = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
```
</details>

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

```diff_python
-def draw_subtree(self, centernode=None, selectednode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
+def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, size=0.25, lw=0.8, maxdepth=2):
+   def calc_darkness(node):
+       if anim_frame is None:
+           return 0
+       index = node.score_index if isscore else node.id
+       return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}
元と同じなので省略
+               darkness = calc_darkness(node)
-               rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
+               rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
+               darkness = calc_darkness(sibling)
-               rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
+               rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
元と同じなので省略
+       darkness = calc_darkness(parent)
-       rect = parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
+       rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
+           darkness = calc_darkness(node)
-           rect = node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
+           rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
```
</details>

### `update_gui` の修正

次に、Marubatsu_Anim クラスの `update_gui` を下記のプログラムのように修正します。

* __4 行目__：`draw_subtree` を呼び出す際に、キーワード引数 `anim_frame` と `isscore` を記述するように修正する
* 5 行目の下にあった、ノードを暗くする処理と赤い外枠を改めて描画する処理を削除する。なお、__赤い外枠を改めて描画する処理__ は、__元のプログラム__ では暗くする処理によって __赤い外枠も暗くなってしまうため行っていた__ が、__`draw_node`__ では __暗く表示する処理の後で外枠を表示する__ ように修正したので __その処理は必要がなくなっている__

```python
1  from tree import Mbtree_Anim
2
3  def update_gui(self):
元と同じなので省略
4      self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
5                               anim_frame=self.play.value, isscore=self.isscore, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
6    
7   Mbtree_Anim.update_gui = update_gui
```

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

```python
from tree import Mbtree_Anim

def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    self.selectednode = self.nodelist[self.play.value]
    self.centernode = self.selectednode
    if self.mbtree.algo == "bf":
        if self.centernode.depth > 0:
            self.centernode = self.centernode.parent
    while self.centernode.depth > 6:
        self.centernode = self.centernode.parent
    if self.centernode.depth <= 4:
        maxdepth = self.centernode.depth + 1
    elif self.centernode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
                             anim_frame=self.play.value, isscore=self.isscore, ax=self.ax, maxdepth=maxdepth)
                            
    disabled = self.play.value == 0
    self.set_button_status(self.prev_button, disabled=disabled)
    disabled = self.play.value == self.nodenum - 1
    self.set_button_status(self.next_button, disabled=disabled)
    
Mbtree_Anim.update_gui = update_gui
```
</details>

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

```diff_python
from tree import Mbtree_Anim

def update_gui(self):
元と同じなので省略
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
-                            ax=self.ax, maxdepth=maxdepth)
+                            anim_frame=self.play.value, isscore=self.isscore, ax=self.ax, maxdepth=maxdepth)
-   for rect, node in self.mbtree.nodes_by_rect.items():
-       index = node.score_index if self.isscore else node.id
-       if index > self.play.value:
-           self.ax.add_artist(patches.Rectangle(xy=(rect.x, rect.y), width=rect.width,
-                                                height=rect.height, fc="black", alpha=0.5))
-       if node is self.selectednode:
-           self.ax.add_artist(patches.Rectangle(xy=(rect.x, rect.y), width=rect.width,
-                                                height=rect.height, ec="red", fill=False, lw=2))

元と同じなので省略
    
Mbtree_Anim.update_gui = update_gui
```
</details>

実行結果はいずれも省略しますが、下記のプログラムを実行して正しい処理が行われることを確認して下さい。

下記は深さ優先アルゴリズムでゲーム木が作成される様子を表示するアニメーションです。

```python
Mbtree_Anim(mbtree)
```

下記は幅優先アルゴリズムでゲーム木が作成される様子を表示するアニメーションです。

```python
bftree = Mbtree.load("../data/bftree")
Mbtree_Anim(bftree)
```

下記は評価値が計算される様子を表示するアニメーションです。

```python
Mbtree_Anim(mbtree, isscore=True)
```

## 最善手以外の局面を暗く表示する処理の実装

話がかなり脱線しましたので、先程説明した __最善手を表示する手順__ を再掲します。

1. ノードを描画する処理を行った後で、最善手を着手した局面であるかを判定する
2. 最善手を着手した局面でないと判定された場合は、[以前の記事](https://qiita.com/ysgeso/items/10449c1d8e1465de1ed1#%E5%8D%8A%E9%80%8F%E6%98%8E%E3%81%AE%E9%BB%92%E3%81%84%E5%9B%B3%E5%BD%A2%E3%82%92%E9%87%8D%E3%81%AD%E3%82%8B%E6%96%B9%E6%B3%95)で説明した方法で、その上に半透明な黒い正方形の画像を描画することでその局面を暗く表示する

また、__最善手を着手した局面であるかの判定方法__ を再掲します。

1. 親ノードが存在しない場合はルートノードなので最善手を着手したと判定する
2. `lastmove` 属性が、親ノードの `bestmoves` 属性の要素に含まれている場合は最善手を着手したと判定する
3. そうでなければ最善手を着手していないと判定する

### `draw_subtree` の修正

上記の処理を行うためには、__最善手以外の着手が行われたノードを暗く表示することができるよう__ に、__`draw_subtree` を修正する__ 必要があります。そこで、下記のプログラムのように __`draw_subtree`__ に `True` が代入されていた場合に最善手以外の着手が行われたノードを暗く表示する仮引数 __`show_bestmove` を追加__ することにします。なお、下記では `show_bestmove` と `anim_frame` の両方が `None` ではない場合は、`show_bestmove` を優先することにしました。

* __2 行目__：デフォルト値を `False` とする仮引数 `show_bestmove` を追加する
* __4 ~ 10 行目__：ノードを表示する暗さを計算する `cacl_darkness` の中で、`show_bestmove` が `True` の場合の暗さを計算する処理を記述する
* __5、6 行目__：親ノードが存在しない場合は最善手を着手したと判定し、暗く表示しないことを表す `0` を返す
* __7、8 行目__：直前の着手が親ノードの最善手の一覧に含まれている場合は、最善手が着手されているので `0` を返す
* __9、10 行目__：上記のどちらでもなければ最善手が着手されていないので、少し暗く表示することを表す `0.2` を返す。なお、`anim_frame` が設定されている際に暗く表示する場合は `0.5` を返していたが、`0.5` だと暗すぎる感じがしたので `0.2` とした。別の暗さで表示したい場合は自由に変更すること

```python
 1  def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False,
 2                   ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 3      def calc_darkness(node):
 4          if show_bestmove:
 5              if node.parent is None:
 6                  return 0
 7              elif node.mb.last_move in node.parent.bestmoves:
 8                  return 0
 9              else:
10                  return 0.2
11          if anim_frame is None:
12              return 0
13          index = node.score_index if isscore else node.id
14          return 0.5 if index > anim_frame else 0
元と同じなので省略    
15            
16  Mbtree.draw_subtree = draw_subtree
```

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

```python
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False,
                 ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        if show_bestmove:
            if node.parent is None:
                return 0
            elif node.mb.last_move in node.parent.bestmoves:
                return 0
            else:
                return 0.2
            
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                darkness = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
```
</details>

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

```diff_python
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False,
-                ax=None, size=0.25, lw=0.8, maxdepth=2):
+                ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
    def calc_darkness(node):
+       if show_bestmove:
+           if node.parent is None:
+               return 0
+           elif node.mb.last_move in node.parent.bestmoves:
+               return 0
+           else:
+               return 0.2
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
元と同じなので省略    
            
Mbtree.draw_subtree = draw_subtree
```
</details>

### `update_gui` メソッドの修正

次に、Mbtree_GUI クラスの `update_gui` の中で、下記のプログラムの 3 行目のように `draw_subtree` を呼び出す際に `show_bestmove=True` を記述するように修正します。

```python
1  def update_gui(self):
元と同じなので省略
2      self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
3                               show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
4    
5  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(0, self.height)   
    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
    centernode = self.selectednode
    while centernode.depth > 6:
        centernode = centernode.parent
    self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
                             show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
    
    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)
    
Mbtree_GUI.update_gui = update_gui
```
</details>

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

```diff_python
def update_gui(self):
元と同じなので省略
    self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
-                            ax=self.ax, maxdepth=maxdepth)
+                            show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
    
Mbtree_GUI.update_gui = update_gui
```
</details>

上記の修正後に下記のプログラムを実行し、別のノードをクリックして選択する[^4]と実行結果の左図ように最善手が着手されていない局面のノードが暗く表示されます。また、実行結果の右図では過去の 3 手分の着手がいずれも最善手ではないことがわかるようになります。

```python
gui_play()
```

実行結果（GUI の部分木のみを表示します）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/016c04aa-ae97-3496-5b52-661afeda03e1.png" width="100">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/5834b8b7-5edf-ad18-e376-b5681dffb3ef.png" width="300">

以上で、最善手を明確にする処理の実装は完了です。

# 最善手のみを着手した場合の部分木の表示

[前回の記事](https://qiita.com/ysgeso/items/b5e388878fef195d8f2c#%E3%80%87%E3%82%B2%E3%83%BC%E3%83%A0%E3%81%AE-gui-%E3%81%A8%E3%82%B2%E3%83%BC%E3%83%A0%E6%9C%A8%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AE%E9%80%A3%E5%8B%95)では、将棋や囲碁の中継などで、プロ棋士の対戦中の局面に対して下図の上部のような「__現在の局面から数手先までの最善手の一覧__」という工夫がされているという話をしました。そこで、〇×ゲームでも、__GUI の部分木に同様の表示を行う__ ことにします。どのように表示を行えば良いかについて少し考えてみて下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/39119a07-375e-63d3-11da-22d48f5b6780.png" width="300">

## 表示の仕様

現状の GUI の部分木では、現在の局面の __深さが浅い場合__ は、下図のように現在の局面の __次の深さのノードまでしか表示されません__ が、__その後に__ そこから決着がつくまで __最善手のみを着手した局面を表示する__ という方法が考えられます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/016c04aa-ae97-3496-5b52-661afeda03e1.png" width="100">

ピンとこない人が多いのではないかと思いますので、完成版の GUI の部分木の表示例を下図に示します。下図では、ゲーム開始時の局面に対する 9 つの __それぞれの合法手を着手した局面__ に対して、その __右の背景が灰色になっている部分__ に __最善手を着手し続けた場合の局面__ を並べて表示しています。このような表示を行うことで、__それぞれの合法手を着手した後__ の __試合の展開と結果が一目でわかる__ ようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/64a3cb35-143c-98fa-0e6d-5f08a786a096.png" width="300">

なお、先程の図の将棋というゲームは決着がつくまでに平均約 100 手程かかるので、3 手先までの最善手の一覧を表示していますが、〇×ゲームは最大でも 9 手までなので、__ゲームの決着がつくまでの最善手のを着手した局面の一覧を表示__ することにしました。

また、__最善手が複数ある場合__ は、__`bestmoves`__ 属性の __最初の最善手のみ__ を着手することにします。そうすることで、__着手の組み合わせ__ が必ず __1 通りになる__ ので、最善手のみを着手した局面を表示しても、GUI の部分木の __縦幅が広がることはありません__。

上記の処理をどのように実装すればよいかについて少し考えてみて下さい。

## `draw_subtree` の修正

__GUI の部分木__ は __`draw_subtree`__ メソッドによって描画を行います。その中で __中心となる `centernode` から `maxdepth` までの部分木を表する__ という処理を行っていますが、その際に __`maxdepth` の深さの子ノードを描画した後__ で、__ゲームの決着がつくまで最善手を着手したノードを表示し続ける__ という処理を記述する必要があります。また、__これまでと同様の方法で部分木を表示できるようにする__ ために、その処理は先ほど追加した __仮引数 `show_bestmove` に `True` が代入されている場合に行う__ ことにします。

下記はそのように `draw_subtree` を修正したプログラムです。

* __4 ~ 12 行目__：2 行目で `node` に代入されたノードを `draw_node` で描画した後で、`show_bestmove` に `True` が代入されており、`node` の深さが `maxdepth` と等しい場合に `node` から最善手のみを着手した場合の局面を表示する処理を行う
* __5 行目__：__`node` に代入された値__ は、13 行目のように __この処理が行われた後で利用する__ ので、__`node` の値を `bestnode` という変数に代入__ し、その変数を使って処理を行う
* __6 行目__：`bestnode` の最善手を表す `bestmoves` 属性に要素が存在する間繰り返し処理を行う。ゲーム中であるばあいは最善手が必ず存在することから、この条件式を `bestnode.mb.status == Marubatsu.PLAYING` としても良い
* __7、8 行目__：`bestmoves` 属性の最初の要素を着手する最善手とし、[`children_by_move`](https://qiita.com/ysgeso/items/7b687d25a4ab300f5865#node-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E4%BF%AE%E6%AD%A3) 属性を利用してその最善手を着手した子ノードを計算して `bestnode` に代入する
* __9 行目__：`bestnode` を表示する位置の x 座標をノードの深さから計算する
* __10 行目__：`draw_subtree` では、最初に `self.calc_node_height(N=centernode, maxdepth=maxdepth)` を実行することで、__深さ `maxdepth` までのノードの表示の高さを計算している__ が、これから表示する __`bestnode` の深さは `maxdepth` よりも深い__ ので __`height` 属性の計算は行っていない__。そのため、__`height` 属性__ に表示するノードの高さを表す __`4` を代入しておく必要がある__[^5]点に注意すること
* __11 行目__：`draw_node` メソッドを使って `bestnode` を表示する。その際に、[`maxdepth` に `bestnode` の深さを代入する](https://qiita.com/ysgeso/items/ae8a782334350cb9c95b#%E9%83%A8%E5%88%86%E6%9C%A8%E3%81%AE%E6%9C%80%E3%82%82%E6%B7%B1%E3%81%84%E3%83%8E%E3%83%BC%E3%83%89%E3%81%AB%E5%AD%90%E3%83%8E%E3%83%BC%E3%83%89%E3%81%8C%E5%AD%98%E5%9C%A8%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AE%E8%A1%A8%E7%A4%BA)ことで `bestnode` に子ノードが存在する場合に右に 1 本だけ線を描画するようにする。また、__`bestnode` は最善手を着手した局面__ なので、__暗く表示する必要はない__ ので、実引数 __`darkness` を記述する必要はない__
* __12 行目__：[`nodes_by_rect`](https://qiita.com/ysgeso/items/7b687d25a4ab300f5865#draw_node-%E3%81%AE%E4%BF%AE%E6%AD%A3)属性に代入された dict に `bestnode` の情報を登録する[^6]

```python
 1  def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 2                  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
 3                  self.nodes_by_rect[rect] = node
 4                  if show_bestmove and depth == maxdepth:
 5                      bestnode = node
 6                      while len(bestnode.bestmoves) > 0:
 7                          bestmove = bestnode.bestmoves[0]
 8                          bestnode = bestnode.children_by_move[bestmove]
 9                          dx = 5 * bestnode.depth
10                          bestnode.height = 4
11                          rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
12                          self.nodes_by_rect[rect] = bestnode
13                  dy += node.height
元と同じなので省略                    
13            
14  Mbtree.draw_subtree = draw_subtree
```

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

```python
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        if show_bestmove:
            if node.parent is None:
                return 0
            elif node.mb.last_move in node.parent.bestmoves:
                return 0
            else:
                return 0.2
            
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                if show_bestmove and depth == maxdepth:
                    bestnode = node
                    while len(bestnode.bestmoves) > 0:
                        bestmove = bestnode.bestmoves[0]
                        bestnode = bestnode.children_by_move[bestmove]
                        dx = 5 * bestnode.depth
                        bestnode.height = 4
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
                        self.nodes_by_rect[rect] = bestnode
                    
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                darkness = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
```
</details>

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

```diff_python
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
+               if show_bestmove and depth == maxdepth:
+                   bestnode = node
+                   while len(bestnode.bestmoves) > 0:
+                       bestmove = bestnode.bestmoves[0]
+                       bestnode = bestnode.children_by_move[bestmove]
+                       dx = 5 * bestnode.depth
+                       bestnode.height = 4
+                       rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
+                       self.nodes_by_rect[rect] = bestnode
                dy += node.height
元と同じなので省略                    
            
Mbtree.draw_subtree = draw_subtree
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果の左図のように、ゲーム開始時の局面のそれぞれの子ノードの後ろに、決着がつくまで最善手を着手し続けた場合の局面が表示されるようになります。右図は、別のノードをクリックして選択した場合のものです。最善手である (1, 1) を着手した場合に引き分けになる着手の例と、それ以外を着手した場合に 〇 が勝利する着手の例が表示されます。

```python
gui_play()
```

実行結果（GUI の部分木のみを表示します）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a3931c62-81d7-4b00-e02e-6c7ea4a04eeb.png" width="300">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/82b04885-d470-bd49-e9b9-39dadbbc4f51.png" width="300">

## 最善手を着手した局面の区別

上図では __どこからが最善手を着手し続けた局面であるかがわかりづらい__ という欠点があります。そこで、__最善手を着手し続けた局面__ の部分の __背景色を灰色で表示__ することで __区別できるようにする__ という工夫を行うことにします。

下記はそのように `draw_subtree` を修正したプログラムです。

* __2 ~ 6 行目__：`show_bestmove` が `True` の場合に、`maxdepth` より深いノードの部分の背景色を灰色で表示するようにする
* __3 行目__：灰色の長方形の x 座標を計算して `bestx` に代入する。この式は、深さ `maxdepth` のノードの x 座標を表す `5 * maxdepth` に様々な数値を足した式を記述してプログラムを実行して表示を確認するという、__試行錯誤によって決めた式__ である
* __4 行目__：__Mbtree_GUI__ では、__Figure の幅を 50 に設定している__ ので、灰色の長方形の表示幅は 50 から `bestx` を引いた値で計算できる
* __5、6 行目__：Figure の中で、`bestx` から右の長方形の範囲を灰色で塗りつぶす

```python
 1  def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 2      if show_bestmove:
 3          bestx = 5 * maxdepth + 4
 4          bestwidth = 50 - bestx
 5          ax.add_artist(patches.Rectangle(xy=(bestx, 0), width=bestwidth,
 6                                          height=height, fc="lightgray"))
 7    
 8      nodelist = [centernode]
 9      depth = centernode.depth
10      while len(nodelist) > 0 and depth <= maxdepth:        
元と同じなので省略
11            
12  Mbtree.draw_subtree = draw_subtree
```

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

```python
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        if show_bestmove:
            if node.parent is None:
                return 0
            elif node.mb.last_move in node.parent.bestmoves:
                return 0
            else:
                return 0.2
            
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    if show_bestmove:
        bestx = 5 * maxdepth + 4
        bestwidth = 50 - bestx
        ax.add_artist(patches.Rectangle(xy=(bestx, 0), width=bestwidth,
                                        height=height, fc="lightgray"))
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                if show_bestmove and depth == maxdepth:
                    bestnode = node
                    while len(bestnode.bestmoves) > 0:
                        bestmove = bestnode.bestmoves[0]
                        bestnode = bestnode.children_by_move[bestmove]
                        dx = 5 * bestnode.depth
                        bestnode.height = 4
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
                        self.nodes_by_rect[rect] = bestnode                                          
                    
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                darkness = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
```
</details>

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

```diff_python
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
+   if show_bestmove:
+       bestx = 5 * maxdepth + 4
+       bestwidth = 50 - bestx
+       ax.add_artist(patches.Rectangle(xy=(bestx, 0), width=bestwidth,
+                                       height=height, fc="lightgray"))
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
元と同じなので省略
            
Mbtree.draw_subtree = draw_subtree
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果の左図のように、最善手を着手し続けた部分の背景色が灰色で表示されるようになります。右図は別の局面を中心とした場合の図です。

```python
gui_play()
```

実行結果（GUI の部分木のみを表示します）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/64a3cb35-143c-98fa-0e6d-5f08a786a096.png" width="300">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3712a6c6-3ac2-a2ed-80f5-9f8bac971804.png" width="300">


これで、GUI のゲーム木から以下の事が簡単にわかるようになりました。

* 現在の局面に対する __合法手の中の最善手__
* それぞれの合法手を着手した局面から、__最善手を着手し続けた場合の決着までの経過__

# ヘルプの表示の ON/OFF

現状では、__〇×ゲームの GUI__ の __? ボタンのヘルプ__ に関して __下記のような問題__ があります。

* ヘルプを表示すると、__ゲームをリセットするまでヘルプの表示が消えない__
* __ヘルプの表示__ と、既にマークが配置されたマスをクリックした際の __メッセージの表示__ が __同じ場所に行われる__

また、また、__GUI の部分木__ の ? ボタンの場合は、一度クリックすると __ヘルプの表示が二度と消えなくなる__ という問題があります。

そこで、__? ボタン__ によって、__ヘルプの表示の有無を切り替える__ ことができるように修正することにします。表示の切り替えは、[前回の記事](https://qiita.com/ysgeso/items/b5e388878fef195d8f2c#ipywidgets-%E3%81%AE%E3%82%A6%E3%82%A3%E3%82%B8%E3%82%A7%E3%83%83%E3%83%88%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%82%92%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88%E3%82%8B%E6%96%B9%E6%B3%95)で説明したように、ウィジェットの  __`layout.display`__ 属性を利用することで行えます。

## Marubatsu_GUI クラスのヘルプの表示の修正

まず、ヘルプの表示と、既にマークが配置されたマスをクリックした際のメッセージの __表示を分けるため__ に、下記のプログラムのように `create_widgets` を修正します。

* __8 行目__：ヘルプを表示する Output を作成し、`help` 属性に代入する
* __9 行目__：ヘルプの表示の有無は、`layout.display` 属性を使って切り替えることにしたので、__ヘルプのメッセージは最初に表示しておけばよい__。ヘルプに表示するメッセージを修正するたびに `create_widgets` メソッドを修正するのは面倒なので、__メッセージを表示する `print_helpmessage` メソッドをこの後で定義__ し、そのメソッドを呼び出すことにする
* __10 行目__：`layout.display` 属性に `"none"` を代入して __最初はヘルプを非表示状態にする__

```python
 1  import ipywidgets as widgets 
 2
 3  def create_widgets(self):
元と同じなので省略
 4      # print による文字列を表示する Output を作成する
 5      self.output = widgets.Output()  
 6    
 7      # ヘルプを表示する Output を作成し、表示の設定を行う
 8      self.help = widgets.Output()
 9      self.print_helpmessage()
10      self.help.layout.display = "none"
11    
12  Marubatsu_GUI.create_widgets = create_widgets
```

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

```python
import ipywidgets as widgets 

def create_widgets(self):
    # 乱数の種の Checkbox と IntText を作成する
    self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
                                    indent=False, layout=widgets.Layout(width="100px"))
    self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
                                layout=widgets.Layout(width="80px"))   

    # 読み書き、ヘルプのボタンを作成する
    self.load_button = self.create_button("開く", 50)
    self.save_button = self.create_button("保存", 50)
    self.show_tree_button = self.create_button("木", 34)
    self.reset_tree_button = self.create_button("リ", 34)
    self.help_button = self.create_button("？", 34)
    
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセット、待ったボタンを作成する
    self.change_button = self.create_button("変更", 50)
    self.reset_button = self.create_button("リセット", 80)
    self.undo_button = self.create_button("待った", 60)    
    
    # リプレイのボタンとスライダーを作成する
    self.first_button = self.create_button("<<", 50)
    self.prev_button = self.create_button("<", 50)
    self.next_button = self.create_button(">", 50)
    self.last_button = self.create_button(">>", 50)     
    self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()

    # print による文字列を表示する Output を作成する
    self.output = widgets.Output()  
    
    # ヘルプを表示する Output を作成し、表示の設定を行う
    self.help = widgets.Output()
    self.print_helpmessage()
    self.help.layout.display = "none"
    
Marubatsu_GUI.create_widgets = create_widgets
```
</details>

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

```diff_python
import ipywidgets as widgets 

def create_widgets(self):
元と同じなので省略
    # print による文字列を表示する Output を作成する
    self.output = widgets.Output()  
    
    # ヘルプを表示する Output を作成し、表示の設定を行う
+   self.help = widgets.Output()
+   self.print_helpmessage()
+   self.help.layout.display = "none"
    
Marubatsu_GUI.create_widgets = create_widgets
```
</details>

### `print_helpmessage` メソッドの定義

次に、下記のプログラムのように `print_helpmessage` メソッドを定義します。行う処理は、`with self.output` を `with self.help` に変更した以外は `create_eventhandler` 内でヘルプを表示する処理と同じです。なお、下記のプログラムには、[前回の記事](https://qiita.com/ysgeso/items/b5e388878fef195d8f2c#%E3%83%9C%E3%82%BF%E3%83%B3%E3%81%AE%E4%BD%9C%E6%88%90%E3%81%A8%E8%A1%A8%E7%A4%BA)で追加した「木」と「リ」ボタンの説明を追記しました。

```python
def print_helpmessage(self):
    with self.help:
        print("""操作説明

    マスの上でクリックすることで着手を行う。
    下記の GUI で操作を行うことができる。
    ()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
    なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。

    乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
    開く(-,L)\tファイルから対戦データを読み込む
    保存(+,S)\tファイルに対戦データを保存する
    木\t下部の GUI の部分木を表示の有無を切り替える
    リ\r株の GUI の部分木の中心となるノードを、現在の局面にリセットする
    ？(*,H)\t\tこの操作説明を表示する
    手番の担当\tメニューからそれぞれの手番の担当を選択する
    \t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
    変更\t\tゲームの途中で手番の担当を変更する
    リセット\t手番の担当を変更してゲームをリセットする
    待った(0)\t1つ前の自分の着手をキャンセルする
    <<(↑)\t\t最初の局面に移動する
    <(←)\t\t1手前の局面に移動する
    >(→)\t\t1手後の局面に移動する
    >>(↓)\t\t最後の着手が行われた局面に移動する
    スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する

    手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
    リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
        
Marubatsu_GUI.print_helpmessage = print_helpmessage
```

### `display_widgets` の修正

次に、`display_widgets` を下記のプログラムのように修正します。

* __3 行目__：VBox の最後に `self.help` を配置するように修正する

```python
1  def display_widgets(self):
元と同じなので省略
2      # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
3      display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output, self.help]))
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])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3, 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):
元と同じなので省略
    # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output, self.help]))
    
Marubatsu_GUI.display_widgets = display_widgets 
```
</details>

### `create_event_handler` の修正

最後に、`create_event_handler` 内で ? ボタンをクリックした際のイベントハンドラを下記のプログラムのように修正します。

* __5 行目__：[前回の記事](https://qiita.com/ysgeso/items/b5e388878fef195d8f2c#create_event_handler-%E3%81%AE%E4%BF%AE%E6%AD%A3)で GUI の部分木の表示の有無を切り替えたのと同じ方法で、ヘルプメッセージの Ouput の表示を切り替える処理を記述する
* 5 行目の下にあった、ヘルプの Output の表示内容をクリアする処理と、ヘルプメッセージを表示する処理は必要が無くなったので削除する

```python
1  import math
2
3  def create_event_handler(self):
元と同じなので省略
4      def on_help_button_clicked(b=None):
5          self.help.layout.display = "none" if self.help.layout.display is None else None
元と同じなので省略           
6        
7  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.mbtree_gui.vbox.layout.display = "none" if self.mbtree_gui.vbox.layout.display is None else None
        
    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_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_help_button_clicked(b=None):
+       self.help.layout.display = "none" if self.help.layout.display is None else None
-       self.output.clear_output()
-       with self.output:
-           print("""操作説明
-           以下の表示の部分は長いので省略
元と同じなので省略           
        
Marubatsu_GUI.create_event_handler = create_event_handler       

```
</details>

実行結果は省略しますが、下記のプログラムを実行して ? ボタンをクリックすることで、ヘルプの表示の切り替えを行うことができるようになったことを確認して下さい。また、既にマークが配置されたマスをクリックした際のメッセージが、ヘルプとは別の Output に表示されるようになったことも確認して下さい。

```python
gui_play()
```

## Mbtree_GUI のヘルプの修正

Mbtree_GUI のヘルプも同様の方法で修正することができます。行う処理は上記と同様なので、修正したプログラムのみを下記に記述します。なお、`self.output` を `self.help` に修正すると、修正箇所が増えてしまうので、`self.output` のままとしました。

```python
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.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   
    
Mbtree_GUI.create_widgets = create_widgets

def print_helpmessage(self):
    with self.output:
        print("""操作説明

    下記のキーとボタンで中心となるノードを移動できる。
    ←、0 キー：親ノードへ移動
    ↑：一つ前の兄弟ノードへ移動
    ↓：一つ後の兄弟ノードへ移動
    →：先頭の子ノードへ移動

    テンキーで、対応するマスに着手が行われた子ノードへ移動する
    ノードの上でマウスを押すことでそのノードへ移動する
    背景が灰色になっている部分のノードは、最善手を着手し続けた場合のノードを表す
    """)
        
Mbtree_GUI.print_helpmessage = print_helpmessage

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_help_button_clicked(b=None):
        self.output.layout.display = "none" if self.output.layout.display is None else None
                        
    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.help_button.on_click(on_help_button_clicked)

    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
```

実行結果は省略しますが、下記のプログラムを実行して GUI の部分木の ? ボタンをクリックすることで、ヘルプの表示の切り替えを行うことができるようになったことを確認して下さい。

```python
gui_play()
```

# 今回の記事のまとめ

今回の記事では、最初に循環参照の問題を解決する方法を紹介しました。

次に、合法手の中の最善手がわかるように GUI の部分木の表示を修正し、合法手を着手した局面から最善手を着手し続けた場合の局面を表示するように修正しました。

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

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

# 次回の記事

https://qiita.com/ysgeso/items/0f83bcdf76bac5b0175f

[^1]:[前回の記事](https://qiita.com/ysgeso/items/b5e388878fef195d8f2c#mbtree_gui-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AB%E3%82%88%E3%82%8B%E9%83%A8%E5%88%86%E6%9C%A8%E3%81%AE%E8%A1%A8%E7%A4%BA)で、ゲーム盤の下に Mbtree_GUI クラスを使って表示する部分木のことを、GUI の部分木 と表記することにしました
[^2]:matplotlib では線（edge（辺））の色を `edgecolor` または `ec` という名前の仮引数に代入しますが、〇×ゲームには外枠の線（border（境界線））だけでなく、ゲーム盤の線も存在するので bordercolor の略の `bc` としました
[^3]:ゲーム盤の線の太さを表す仮引数 `lw` が既に存在するので、こちらは borderwidth の略の `bw` としました
[^4]:ゲーム開始時の局面は、すべての合法手が最善手なのでこれまでと表示がかわりません。そのため、別のノードを選択しました
[^5]:筆者もこの処理を最初は記述し忘れてしまい、エラーが発生しました
[^6]:この処理は、ノードをクリックすることでそのノードを中心となるノードに移動する処理を行うためのものです