Pythonで〇×ゲームのAIを一から作成する　その139　Mbtree_Anim クラスのバグの修正と改良

# 目次と前回の記事

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

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

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

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

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

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

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

# Mbtree_Anim のバグの修正と改良

前回の記事で αβ 法による評価値の計算を紹介しましたが、__αβ 法__ が行う処理の手順は __直観的にわかりづらい__ と思った人が多いのではないかと思います。そこで、以前の記事で __Mbtree_Anim__ クラスを作成して幅優先アルゴリズムや深さ優先アルゴリズムで __[ゲーム木が作成される手順](https://qiita.com/ysgeso/items/c82c854c529172f74103)__ や __[評価値が計算される手順](https://qiita.com/ysgeso/items/c82c854c529172f74103)__ を __アニメーションで視覚化__ したように、αβ 法によって評価値が計算される手順も Mbtree_Anim で視覚化できるようにすることにします。

その前に、Mbtree_Anim にバグがあることが判明したので、今回の記事ではその修正を行うことにします。また、Mbtree_Anim の表示の改良を行うことにします。

## Mbtree_Anim クラスで新たに発生したバグ

下記は、[以前の記事](https://qiita.com/ysgeso/items/3400391d2637eafc8899#%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E4%BF%9D%E5%AD%98)で `dfscore.mbtree` に保存した、深さ優先アルゴリズムで評価値を計算した __ゲーム木の評価値の計算過程を Mbtree_Anim で表示__ するプログラムです[^1]。

```python
from tree import Mbtree, Mbtree_Anim

mbtree = Mbtree.load("../data/dfscore")
Mbtree_Anim(mbtree, isscore=True)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/462a915d-6307-132d-6383-6d9477448550.png" width="500">

実行結果から、以下の問題がある事がかわかります。

* __一番上__ の局面の __評価値の表示__ が、Figure の外に __はみ出して見えなくなっている__
* 評価値の __文字が大きすぎて__、上部が一つ上の局面に __重なっている__

### バグの原因の検証

この問題は、[以前の記事](https://qiita.com/ysgeso/items/94c5105c3dbebb445710#%E9%81%B8%E6%8A%9E%E3%81%95%E3%82%8C%E3%81%9F%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AE%E6%94%B9%E8%89%AF)で dfscore.mbtree を Mbtree_Anim で表示した際には、局面の上に __評価値を表示していなかった__ ため、__発生していませんでした__。

[その後の記事](https://qiita.com/ysgeso/items/0f83bcdf76bac5b0175f#%E8%A9%95%E4%BE%A1%E5%80%A4%E3%81%AE%E8%A1%A8%E7%A4%BA) で Mbtree クラスの `draw_subtree` メソッドを修正して __局面の上に評価値を表示する__ ように修正した時点でこの __バグが発生していた__ のですが、その際に __Mbtree_Anim での表示__ が正しく行われることを __確認していなかった__ ため、__今回の記事まで__ そのようなバグがあることが __判明していませんでした__。

一番上の局面の評価値の __表示がはみ出る問題__ は、__[以前の記事](https://qiita.com/ysgeso/items/0f83bcdf76bac5b0175f#%E4%B8%8A%E9%83%A8%E3%81%AE%E8%A9%95%E4%BE%A1%E5%80%A4%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AE%E4%B8%80%E9%83%A8%E3%81%8C%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E5%95%8F%E9%A1%8C%E3%81%AE%E4%BF%AE%E6%AD%A3)__ で同じ問題に __対処した方法と同じ方法__ で修正することができます。

__評価値の文字の表示__ は Mbtree クラスの __`draw_subtree`__ メソッドで行っており、評価値の __文字の大きさは仮引数 `size`__ に代入された値 __で決まります__。現状の Mbtree_Anim クラスは `update_gui` メソッド内で __`draw_subtree` メソッドを呼び出す際__ に、仮引数 __`size`__ に対応する __実引数を記述していないため__、適切な大きさで評価値が表示されません[^2]。そのため、この問題は __`size=self.size`__ を実引数に記述して `draw_subtree` メソッドを呼び出すように修正することで解決することができます。

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

まず、__Mbtree_Anim__ クラスの __`__init__`__ メソッドを下記のプログラムのように修正します。この修正について忘れた方は、[以前の記事](https://qiita.com/ysgeso/items/0f83bcdf76bac5b0175f#mbtree_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)を復習してください。

* __6 行目__：局面の上に評価値を表示するようになったので、その分だけ表示の高さを表す `height` 属性の値を 64 から 65 に増やす
* __8 行目__：クラスの外で `__init__` メソッドを定義できるように、`super` を修正する。この修正について忘れた方は [以前の記事](https://qiita.com/ysgeso/items/903dc60e865ff56191fc#super-%E3%81%AE%E4%BF%AE%E6%AD%A3%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%AA%AC%E6%98%8E) を復習すること

```python
 1  def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
 2      self.mbtree = mbtree
 3      self.isscore = isscore
 4      self.size = size
 5      self.width = 50
 6      self.height = 65
 7      self.nodelist = self.mbtree.nodelist_by_score if isscore else self.mbtree.nodelist 
 8      self.nodenum = len(self.nodelist)
 9      super(Mbtree_Anim, self).__init__()
10      
11  Mbtree_Anim.__init__ = __init__
```

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

```python
def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
    self.mbtree = mbtree
    self.isscore = isscore
    self.size = size
    self.width = 50
    self.height = 65
    self.nodelist = self.mbtree.nodelist_by_score if isscore else self.mbtree.nodelist 
    self.nodenum = len(self.nodelist)
    super(Mbtree_Anim, self).__init__()
    
Mbtree_Anim.__init__ = __init__
```
</details>

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

```diff_python
def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
    self.mbtree = mbtree
    self.isscore = isscore
    self.size = size
    self.width = 50
-   self.height = 64
+   self.height = 65
    self.nodelist = self.mbtree.nodelist_by_score if isscore else self.mbtree.nodelist 
    self.nodenum = len(self.nodelist)
-   super().__init__()
+   super(Mbtree_Anim, self).__init__()
    
Mbtree_Anim.__init__ = __init__
```
</details>

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

次に、__`update_gui`__ メソッドを下記のプログラムのように修正します。

* __4 行目__：Axes の y 軸方向の表示範囲を -1 だけずらして、一番上の局面の評価値が表示されるように修正する。この修正について忘れた方は [以前の記事](https://qiita.com/ysgeso/items/0f83bcdf76bac5b0175f#update_gui-%E3%81%AE%E4%BF%AE%E6%AD%A3) を復習すること
* __7 行目__：`draw_subtre` メソッドを呼び出す際に、キーワード引数 `size=self.size` を記述するようにすることで、評価値の文字が適切な大きさで表示されるように修正する

```python
1  def update_gui(self):
2      self.ax.clear()
3      self.ax.set_xlim(-1, self.width - 1)
4      self.ax.set_ylim(-1, self.height - 1)   
元と同じなので省略
5      self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
6                               anim_frame=self.play.value, isscore=self.isscore, 
7                               ax=self.ax, maxdepth=maxdepth, size=self.size)
元と同じなので省略
8    
9  Mbtree_Anim.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")   
    
    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, size=self.size)
                            
    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
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
-   self.ax.set_ylim(0, self.height)   
+   self.ax.set_ylim(-1, self.height - 1)   
元と同じなので省略
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
                             anim_frame=self.play.value, isscore=self.isscore, 
-                            ax=self.ax, maxdepth=maxdepth, size=self.size)
+                            ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
    
Mbtree_Anim.update_gui = update_gui
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果のように問題が修正されたことが確認できます。

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

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e2698c7f-941c-dfbc-2f7a-d929e864f792.png" width="500">

下記のプログラムのように __キーワード引数 `size=0.5`__ を記述して __別の大きさで表示__ した場合でも、実行結果のように評価値の文字が適切な大きさで表示されることが確認できます。

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

実行結果（大きすぎるので上部のみの画像です）


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/809f6885-7aff-3bf2-f158-ca4b18762112.png" width="500">

## Mbtree_Anim の改良

現状の Mbtree_Anim では、まだ __評価値を計算していない局面__ を __灰色で表示__ しますが、上図のように __評価値を計算していない局面__ に __評価値が表示__ されてしまう点が __不自然__ です。そこで、__評価値を計算していない局面__ の __評価値を表示しない__ ように改良することにします。

局面の __評価値の表示__ は、Node クラスの __`draw_node`__ メソッドの __仮引数 `show_score` に `True` が代入__ されている場合に行われます。`draw_node` メソッドは __`draw_subtree` メソッド内で呼び出されている__ ので、__`draw_subtree` 内で `draw_node` を呼び出す際__ に、キーワード引数 __`show_score` に適切な値を代入する__ ようにプログラムを修正すれば良いことがわかります。

__`draw_node`__ メソッドは、`draw_subtree` 内の 5 箇所で、下記のプログラムのように記述されて呼び出されているので、それらのプログラムを __ノードの評価値が計算されている場合は `show_score=True`__、__そうでない場合は `show_score=False`__ を実引数に記述して `draw_node` を呼び出すように修正すればよいことがわかります。どのように修正すれば良いかについて少し考えてみて下さい。

```python
    darkness = calc_darkness(node)
    rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                          show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
```

### `calc_darkness` の復習

上記の __`darkness`__ は、__局面の表示の暗さ__ を表す値で、__`calc_darkness` によって計算__ されます。__Mbtree_Anim から `draw_subtree`__ を呼び出して __ゲーム木の部分木を表示する際__ には、__評価値が計算されていない局面を暗く__ 表示し、__計算された局面を明るく__ 表示します。

従って、__`darkness` の値が暗い場合に `show_score=False`__ を、__そうでない場合は `show_score=True`__ を記述して `draw_node` を呼び出すようにすれば良いことがわかります。

__`calc_darkness`__ は[以前の記事](https://qiita.com/ysgeso/items/6d1430a88e251279d77a#draw_subtree-%E3%81%AE%E4%BF%AE%E6%AD%A3-1)で定義した関数ですが、忘れた方も多いと思いますので、その処理を復習することにします。下記は、`calc_darkness` の定義です。

```python
 1  def calc_darkness(node):
 2      if show_bestmove:
 3          if node.parent is None:
 4              return 0
 5          elif node.mb.last_move in node.parent.bestmoves:
 6              return 0
 7          else:
 8              return 0.2
 9           
10      if anim_frame is None:
11          return 0
12      index = node.score_index if isscore else node.id
13      return 0.5 if index > anim_frame else 0
```

まず、__Mbtree_Anim から `draw_subtree`__ が呼び出された際の __`calc_darkness` の処理を復習__ します。__`Mbtree_Anim`__ では、以下のプログラムで __`draw_subtree` を呼び出します__。

```python
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
                             anim_frame=self.play.value, isscore=self.isscore, ax=self.ax,
                             maxdepth=maxdepth)
```

また、__`draw_subtree` の定義__ は以下のプログラムのようになっているので、__Mbtree_Anim から `draw_subtree`__ を呼び出した場合は、仮引数 __`show_bestmove` には `False`__ が、__`anim_frame`__ にはアニメーションの __フレーム番号を表す数値__ が代入されます。

```python
def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
                 isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8,
                 maxdepth=2):
```

従って、__Mbtree_Anim から `draw_subtree`__ を呼び出した場合は、__`calc_darkness`__ の中の __下記のプログラムだけが実行__ されます。__評価値の計算過程を表示__ する場合は、Mbtree_Anim のインスタンスを作成する際に __`isscore=True`__ を記述するので、下記の 2 行目で __ノードの評価値が計算されていない場合は 0.5__ が、__計算されている場合は 0 を返す__ という処理が行われていることがわかります。

```python
1  index = node.score_index if isscore else node.id
2  return 0.5 if index > anim_frame else 0
```

### 推奨しないプログラムの修正方法

上記から、ノードの __評価値が計算されていない場合__ は __`darkness` に 0.5 が代入__ されるので、`draw_subtree` 内で __`draw_node` を呼び出す前__ に、下記のプログラムの 2、3 行目を追加して、ノードの評価値が計算されていないことを表す __`darkness` に 0.5 が代入されている場合__ に __`show_score` に `False` を代入__ すればよいと思った人がいるかもしれませんが、__このような修正は行わないほうが良い__ でしょう。その理由について少し考えてみて下さい。

```python
1  darkness = calc_darkness(node)
2  if darkness == 0.5:
3      show_score = False
4  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
5                        show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
```            

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

```diff_python
    darkness = calc_darkness(node)
+   if darkness == 0.5:
+       show_score = False
    rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                          show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
```
</details>

上記の修正で、確かにプログラムは正しく動作しますが、以下のような問題があります。

第一に、__現状の `calc_darkness` が 0.5 を返り値として返す__ のは、__Mbtree_Anim から `draw_subtree`__ が呼び出された際に、__ノードの評価値が計算されていない場合だけ__ ですが、下記のプログラムの 8 行目のように、__それ以外の場合で 0.5 を返す__ ように修正すると __プログラムが正しく動作しなくなります__。その理由について少し考えてみて下さい。

```python
 1  def calc_darkness(node):
 2      if show_bestmove:
 3          if node.parent is None:
 4              return 0
 5          elif node.mb.last_move in node.parent.bestmoves:
 6              return 0
 7          else:
 8              return 0.5
 9           
10      if anim_frame is None:
11          return 0
12      index = node.score_index if isscore else node.id
13      return 0.5 if index > anim_frame else 0
```

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

```diff_python
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
+           return 0.5
           
    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
```
</details>

例えば、__Mbtree_GUI から `draw_subtree`__ を呼び出す場合などで、__`show_bestmove` の値が `True` になる場合__ があります。その場合は __最善手でない着手が行われた局面__ に対して 8 行目の処理が実行され、__返り値として 0.5 が返される__ ことになります。その結果、__最善手でない着手が行われた局面__ の __評価値が表示されなくなってしまう__ という問題が発生します。

このように、__0.5 という数値__ で評価値が計算されていないノードであるかどうかを __判定する__ と、__別の理由で 0.5 という数値が計算__ された場合と __処理が混同__ してしまいます。

第二に、評価値が計算されていない局面の __暗さは 0.5 でなくても構わない__ ので、例えば __暗さを 0.5 から 0.6 に変更しようと思った場合__ に行う __修正__ が、__`calc_darkness`__ の最後の行を下記のプログラムのように __変更するだけでは済まない__ という問題があります。

```python
    return 0.6 if index > anim_frame else 0
```

具体的には、__`draw_node` を呼び出す直前__ の下記の 2 行目のプログラムの __条件式の 0.5 を 0.6 に修正する必要__ があります。さらに、__`draw_subtree`__ 内では __`draw_node` を 5 箇所で呼び出している__ ので、__その全ての場所__ で下記の 2 行目のような __修正を行う必要がある点が面倒__ です。また、__修正し忘れによるバグが発生する可能性が高い__ という問題もあります。

```python
1  darkness = calc_darkness(node)
2  if darkness == 0.6:
3      show_score = False
4  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
5                        show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
```  

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

```diff_python
    darkness = calc_darkness(node)
-   if darkness == 0.5:
+   if darkness == 0.6:
        show_score = False
    rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                          show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
```
</details>

### `calc_darkness` の修正

上記の問題が発生する原因は、__局面の表示の暗さ__ と、__評価値の表示の有無__ を __同じ `darkness`__ という変数の値で __無理に表現しようとした点__ にあります。__`calc_darkness`__ が __局面の表示の暗さ__ と、__評価値の表示の有無__ の __両方を計算して返す__ ようにすることで、うまく処理を行うことができるようになります。本記事では、__0 番の要素__ にこれまでに計算していた __局面の暗さを表す数値__、__1 番の要素__ に __評価値を表示するか__ どうかを表す bool 型のデータを持つ __tuple を返り値として返す__ ように `calc_darkness` を修正することにします。下記はそのように `calc_darkness` を修正したプログラムです。

* __4、6、8、10 行目__：Mbtree_Anim 以外から `draw_subtree` が呼び出された場合は、__評価値の表示の有無は `show_score` の値によって決まる__ ので、__その値をそのまま tuple の 1 番の要素__ として返すように修正する
* __13 行目__：評価値が計算されていないノードの場合は、tuple の 1 番の要素に `False` を、そうでない場合は `True` を返すように修正する

```python
 1  def calc_darkness(node):
 2      if show_bestmove:
 3          if node.parent is None:
 4              return 0, show_score
 5          elif node.mb.last_move in node.parent.bestmoves:
 6              return 0, show_score
 7          else:
 8              return 0.2, show_score
 9           
10      if anim_frame is None:
11          return 0, show_score
12      index = node.score_index if isscore else node.id
13      return (0.5, False) if index > anim_frame else (0, True)
```
</details>

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

```diff_python
    def calc_darkness(node):
        if show_bestmove:
            if node.parent is None:
-               return 0
+               return 0, show_score
            elif node.mb.last_move in node.parent.bestmoves:
-               return 0
+               return 0, show_score
            else:
-               return 0.2
+               return 0.2, show_score
            
        if anim_frame is None:
-           return 0
+           return 0, show_score
        index = node.score_index if isscore else node.id
-       return 0.5 if index > anim_frame else 0
+       return (0.5, False) if index > anim_frame else (0, True)
```
</details>

:::note
行う処理が変わったので、`calc_darkness` の名前を `calc_darkness_and_showmove` のように修正したほうが良いかもしれませんが、関数名が長くなるので本記事では名前を変更しないことにします。変更したい人は自由に変更して下さい。
:::

:::note alert
13 行目を下記のプログラムのように、返り値を表す __tuple の `()` を省略__ して記述すると __エラーが発生する__ 点に注意して下さい。

```python
    return 0.5, False if index > anim_frame else 0, show_score
```

その理由は、上記のプログラムが下記のプログラムのように、__`0.5`__、__`False if index > anim_frame else 0`__、__`show_score`__ という __3 つの要素を持つ tuple__ を返すからです。

```python
    return (0.5, False if index > anim_frame else 0, show_score)
```

そのことは、下記のプログラムで確認できます。

```python
index = 1
anim_frame = 0 
show_score = True
retval = 0.5, False if index > anim_frame else 0, show_score
print(retval)
```

実行結果

```
(0.5, False, True)
```
:::

### `show_subtree` の修正

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

* __3、9、12、15 行目__：`cald_darkness` の返り値を `darkness` と `show_score` に代入するように修正する
* __7 行目__ の `draw_node` は [以前の記事](https://qiita.com/ysgeso/items/6d1430a88e251279d77a#%E6%9C%80%E5%96%84%E6%89%8B%E3%81%AE%E3%81%BF%E3%82%92%E7%9D%80%E6%89%8B%E3%81%97%E3%81%9F%E5%A0%B4%E5%90%88%E3%81%AE%E9%83%A8%E5%88%86%E6%9C%A8%E3%81%AE%E8%A1%A8%E7%A4%BA)で記述した __`maxdepth` の深さの局面から最善手を着手し続けた場合のノードの表示を行う__ 処理なので、__Mbtree_Anim から `draw_subtree`__ を呼び出した場合は __実行されない__。そのため、7 行目の直前で __`calc_darkness`__ を呼び出して `darkness` と `show_score` の計算を __行う必要はない__。また、このノードは常に明るく表示するため、7 行目の `draw_node` にはキーワード引数 `darkness` は記述されていない
```python
 1  def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
 2                   isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 3                  darkness, show_score = calc_darkness(node)
 4                  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
 5                                        show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
 6                          emphasize = bestnode is selectednode
 7                          rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize,
 8                                                    show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
 9                  darkness, show_score = calc_darkness(sibling)
10                  rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness,
11                                           show_score=show_score, lw=lw, dx=dx, dy=dy)
元と同じなので省略
12          darkness, show_score = calc_darkness(parent)
13          rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, 
14                                  show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
15              darkness, show_score = calc_darkness(node)
16              rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness,
17                                    show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
18              self.nodes_by_rect[rect] = node
19            
20  Mbtree.draw_subtree = draw_subtree
```

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

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

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    if show_bestmove:
        width = 5 * 10
    else:
        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 + 1) * size))
        ax.set_xlim(0, width)
        ax.set_ylim(-1, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    if show_bestmove:
        bestx = 5 * maxdepth + 4
        bestwidth = 50 - bestx
        ax.add_artist(patches.Rectangle(xy=(bestx, -1), width=bestwidth,
                                        height=height + 1, 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, show_score = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                                    show_score=show_score, 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
                        emphasize = bestnode is selectednode
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize,
                                                show_score=show_score, 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, show_score = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness,
                                        show_score=show_score, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness, show_score = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, 
                                show_score=show_score, 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, show_score = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness,
                                show_score=show_score, 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, anim_frame=None,
                 isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
-               darkness = calc_darkness(node)
+               darkness, show_score = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                                      show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
                        emphasize = bestnode is selectednode
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize,
                                                  show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
-               darkness  = calc_darkness(sibling)
+               darkness, show_score  = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness,
                                         show_score=show_score, lw=lw, dx=dx, dy=dy)
元と同じなので省略
-       darkness = calc_darkness(parent)
+       darkness, show_score = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, 
                                show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
-           darkness = calc_darkness(node)
+           darkness, show_score = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness,
                                show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果のように、評価値が計算されていない局面の上に評価値が表示されなくなったことが確認できます。

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

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/9a485c1e-690a-e666-4ea1-8720c7818a96.png" width="500">

また、アニメーションのフレームを進めると、下図のように評価値が計算されている局面の上に評価値が表示されることが確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d400321f-bd73-173f-be34-109dffb836a9.png" width="500">

## ゲーム木の作成の経過の表示の確認

上記で Mbtree_Anim の実引数に __`isscore=True`__ を記述して、__評価値の計算過程を表示__ するアニメーションが正しく表示されることが確認できました。

プログラムを修正した結果、Mbtree_Anim の実引数に __`isscore=True` を記述しない場合に__ 表示される __ゲーム木の作成の過程の表示__ に __バグが発生している可能性がある__ ので、下記のプログラムを実行して __確認する__ ことにします。

```python
Mbtree_Anim(mbtree)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/f2dbaf43-30d5-7e31-6144-303aa982c4f7.png" width="500">

実行結果から、__最初に作成されたルートノードが明るく表示__ され、それ以外の __まだ作成されていないノードが暗く表示される__ ことが確認できます。その際に、作成されているノードの上に評価値が表示されていますが、この __評価値は表示しないほうが良い__ でしょう。その理由について少し考えてみて下さい。

### 評価値を計算しないほうが良い理由

__評価値が計算される順番__ は、ゲーム木の __ノードが作成される順番と大きく異なります__。例えば __ルートノードの評価値が計算される__ のは、__他の全てのノードの評価値が計算された後__ なので、__最初に__ ルートノードが作成された時点で __ルートノードの評価値が表示される__ のは順番が __間違っています__。上記のアニメーションでは __ノードが作成された順番で評価値が表示される__ ため、__評価値の計算の順番に対する誤解が生じる可能性が高い__ でしょう。

そこで、誤解が生じないようにするために、Mbtree_Anim で __ゲーム木の生成の過程__ のアニメーションを表示する場合は、__評価値を表示しない__ ように修正することにします。どのように修正すれば良いかについて少し考えてみて下さい。

### Mbtree クラスの `draw_subtree` の修正

行いたい修正は、__`calc_darkness`__ で局面を __明るく表示すると判定__ した際に、__`isscore` が `True` の場合は評価値を表示__ し、__そうでない場合は評価値を表示しない__ ような __返り値を計算する__ というものです。

`calc_darkness` で __局面を明るく表示すると判定__ した場合に __返す値__ は、下記のプログラムの else の直後の __`(0, True)`__ という tuple です。

```python
    return (0.5, False) if index > anim_frame else (0, True)
```

この部分を __`isscore` が `True`__ の場合に __`(0, True)`__、__`False`__ の場合に __`(0, False)`__ を返すようにするためには、下記のプログラムのように修正します。

```python
    return (0.5, False) if index > anim_frame else (0, isscore)
```

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

* __5 行目__：上記のように修正する

```python
1  def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
2                   isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
3      def calc_darkness(node):
元と同じなので省略
4          index = node.score_index if isscore else node.id
5          return (0.5, False) if index > anim_frame else (0, isscore)
元と同じなので省略 
6            
7  Mbtree.draw_subtree = draw_subtree
```

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

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

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    if show_bestmove:
        width = 5 * 10
    else:
        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 + 1) * size))
        ax.set_xlim(0, width)
        ax.set_ylim(-1, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    if show_bestmove:
        bestx = 5 * maxdepth + 4
        bestwidth = 50 - bestx
        ax.add_artist(patches.Rectangle(xy=(bestx, -1), width=bestwidth,
                                        height=height + 1, 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, show_score = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                                    show_score=show_score, 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
                        emphasize = bestnode is selectednode
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize,
                                                show_score=show_score, 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, show_score  = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness,
                                        show_score=show_score, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness, show_score  = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, 
                                show_score=show_score, 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, show_score  = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness,
                                show_score=show_score, 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, anim_frame=None,
                 isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
元と同じなので省略
        index = node.score_index if isscore else node.id
-       return (0.5, False) if index > anim_frame else (0, True)
+       return (0.5, False) if index > anim_frame else (0, isscore)
元と同じなので省略 
            
Mbtree.draw_subtree = draw_subtree
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果のように __ゲーム木の作成過程を表示__ した場合に __明るいノードの上に評価値が表示されなくなった__ ことが確認できます。

```python
Mbtree_Anim(mbtree)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7c96714f-805f-c303-0d71-ebad47c522c0.png" width="500">

また、実行結果は先ほどと同じなので省略しますが、下記のプログラムを実行して __評価値の計算過程を表示__ した場合は、アニメーションのフレームを進めると __明るいノードの上に評価値が表示される__ ことが確認できます。

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

# 今回の記事のまとめ

今回の記事では、Mbtree_Anim クラスのバグの修正と改良を行いました。

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

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

# 次回の記事

https://qiita.com/ysgeso/items/3de5d3316b78d4925351

[^1]:4 行目で、`isscore=True` を記述することで、評価値の計算過程を表示します。忘れた方は[以前の記事](https://qiita.com/ysgeso/items/94c5105c3dbebb445710#%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%87%A6%E7%90%86%E3%81%AE%E5%AE%9F%E8%A3%85)を復習して下さい。
[^2]:[以前の記事](https://qiita.com/ysgeso/items/94c5105c3dbebb445710#%E9%81%B8%E6%8A%9E%E3%81%95%E3%82%8C%E3%81%9F%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AE%E6%94%B9%E8%89%AF)では、局面の上に評価値を表示していなかったので、仮引数 `size` に適切な値を代入する必要はありませんでした