Pythonで〇×ゲームのAIを一から作成する　その95　ゲーム木を描画する GUI の改良

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/0335cc42055729d0fac3

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

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

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

## ルールベースの AI の一覧

ルールベースの AI の一覧については、下記の記事を参照して下さい。

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

# ゲーム木を視覚化する GUI の改良

前回の記事で作成した、ゲーム木を視覚化する GUI を実装した Mbtre_GUI クラスには、いくつかの問題点があるので、今回の記事では Mbtree_GUI を改良することにします。

## 中心となるノードの強調表示

Mbtree_GUI は、下図のような部分木を描画しますが、下図では、__どのノードを中心とした部分木を描画しているかがわかりづらい__ という問題があります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/ec25b7ba-0abe-8783-b929-b1d142064858.png" width="300">

そこで、__中心となるノード__ を何らかの方法で __強調して描画__ することにします。ノードの強調の方法には、下記のような様々な方法が考えられます。

* 色のついた外枠を描画する
* 枠線やマークを太くする
* 背景色を変える

本記事では、中心となるノードの __外枠を赤色で描画する__ 方法を採用しますが、上記の方法に限らず、もっと良い強調の方法を思いついた人は実装してみて下さい。

### `draw_board` の修正

__ゲーム盤__ は、Marubatsu_GUI クラスの __`draw_board`__ メソッドで描画するので、`draw_board` を __ゲーム盤の外枠を描画できるように改良する__ という方法が考えられます。そこで、下記のように `draw_board` を改良することにします。

* `draw_board` に、`True` が代入されていた場合に赤枠を描画して強調する `emphasize`[^1] という仮引数を追加する
* 互換性を考慮して、`emphasize` はデフォルト値を `False` とするデフォルト引数にする

ゲーム盤の枠は、正方形の形をしているので、[以前の記事](https://qiita.com/ysgeso/items/ae8a782334350cb9c95b#%E3%82%B2%E3%83%BC%E3%83%A0%E7%9B%A4%E3%81%AE%E8%83%8C%E6%99%AF%E8%89%B2%E3%81%AE%E5%A4%89%E6%9B%B4%E6%96%B9%E6%B3%95)で説明した patches モジュールで定義された、長方形を表す __`Rectangle`__ という Artist を作成する関数を利用します。

また、`draw_board` では、下記のプログラムのように、決着がついた __ゲーム盤の背景色を変更するため__ に、`Rectangle` を使っています。

```python
rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                         height=mb.BOARD_SIZE, fc=bgcolor)
```                                    

ゲーム盤の外枠は、上記の `Rectangle` と __同じ場所と大きさで、枠だけを描画すればよい__ ので、下記のプログラムのように、赤色の枠線を表す [`ec="red"`](https://qiita.com/ysgeso/items/ab2770693c947c108dd0#%E6%9E%A0%E3%81%A8%E5%A1%97%E3%82%8A%E3%81%A4%E3%81%B6%E3%81%97%E3%81%AE%E8%A8%AD%E5%AE%9A%E6%96%B9%E6%B3%95)、塗りつぶさないことを表す `fill=False`、枠線の太さを表す `lw=lw` を記述することで作成することができます。

なお、外枠の太さは、内枠やマークの線の太さと同じ `lw` にしました。

```python
rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
                         height=mb.BOARD_SIZE, ec="red", fill=False, lw=lw)
```                                    

下記は、`draw_board` をそのように修正したプログラムです。なお、__外枠を描画する処理__ は、__ゲーム盤の背景色を変更する処理の後__ で行わないと、描画した __外枠の一部が背景色の長方形で上書きされてしまう__ 点に注意する必要があります。

* __5 行目__：デフォルト値が `False` のデフォルト引数 `emphasize` を追加する
* __10 ~ 13 行目__：`emphasize` が `True` の場合に、赤色でゲーム盤の外枠を描画する

```python
 1  from marubatsu import Marubatsu, Marubatsu_GUI
 2  import matplotlib.patches as patches
 3
 4  @staticmethod
 5  def draw_board(ax, mb, show_result=False, emphasize=False, dx=0, dy=0, lw=2):
 6      # 結果によってゲーム盤の背景色を変更する
 7      if show_result:
元と同じなので省略
 8       
 9      # emphasize が True の場合は赤色の外枠を描画する
10      if emphasize:
11          frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
12                                  height=mb.BOARD_SIZE, ec="red", fill=False, lw=lw)
13          ax.add_patch(frame)
元と同じなので省略
14            
15  Marubatsu_GUI.draw_board = draw_board
```

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

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

@staticmethod
def draw_board(ax, mb, show_result=False, emphasize=False, dx=0, dy=0, lw=2):   
    # 結果によってゲーム盤の背景色を変更する
    if show_result:
        if mb.status == Marubatsu.PLAYING:
            bgcolor = "white"
        elif mb.status == Marubatsu.CIRCLE:
            bgcolor = "lightcyan"
        elif 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)
        
    # 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)
    
    # ゲーム盤の枠を描画する
    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)
            
Marubatsu_GUI.draw_board = draw_board
```
</details>

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

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

@staticmethod
-def draw_board(ax, mb, show_result=False, dx=0, dy=0, lw=2):
+def draw_board(ax, mb, show_result=False, emphasize=False, dx=0, dy=0, lw=2):
    # 結果によってゲーム盤の背景色を変更する
    if show_result:
元と同じなので省略
        
+   # 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)
元と同じなので省略
            
Marubatsu_GUI.draw_board = draw_board
```
</details>

### `draw_node` の修正

__`draw_board`__ は、Node クラスの __`draw_node` メソッド内で呼び出される__ ので、`draw_node` に対しても同様に、下記のプログラムのように仮引数 `emphasize` を追加し、`emphasize` に `True` が代入されていた場合にノードを強調して描画するように修正する必要があります。

* __4 行目__：デフォルト値が `False` のデフォルト引数 `emphasize` を追加する
* __7 行目__：`emphasize=emphasize` を記述して `draw_board` を呼び出すように修正する

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

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

```python
from tree import Node
import matplotlib.pyplot as plt

def draw_node(self, ax=None, maxdepth=None, emphasize=False, 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
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, emphasize=emphasize, lw=lw, dx=dx, dy=y)
    # 子ノードが存在する場合に、エッジの線と子ノードを描画する
    if len(self.children) > 0:
        if maxdepth != self.depth:   
            plt.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, dx=dx+5, dy=childnodey, lw=lw)
                edgey = childnodey + 1.5
                plt.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="k", lw=lw)
                if prevy is not None:
                    plt.plot([dx + 4 , dx + 4], [prevy, edgey], c="k", lw=lw)
                prevy = edgey
                dy += childnode.height
        else:
            plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
            
Node.draw_node = draw_node
```
</details>

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

```diff_python
from tree import Node
import matplotlib.pyplot as plt

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

### `draw_subtree` の修正

最後に、__`draw_node` を呼び出す `draw_subtree`__ を下記のプログラムのように修正します。

* __10、11 行目__：強調表示するのは `centernode` だけなので、`draw_node` を呼び出す `node` が `centernode` の場合のみ `emphasize` を `True` にし、`emphasize=emphasize` を実引数に記述して `draw_node` を呼び出すように修正する

```python
 1  from tree import Mbtree
 2
 3  def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 4          for node in nodelist:
 5              if node is None:
 6                  dy += 4
 7                  childnodelist.append(None)
 8              else:
 9                  dx = 5 * node.depth
10                  emphasize = node is centernode
11                  node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
12            
13  Mbtree.draw_subtree = draw_subtree
```

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

```python
from tree import Mbtree

def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
    if centernode is None:
        centernode = self.root
    self.calc_node_height(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 centernode
                node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
                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
                sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
            dy += sibling.height
        dx = 5 * parent.depth
        parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
            
Mbtree.draw_subtree = draw_subtree
```
</details>

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

```diff_python
from tree import Mbtree

def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
+               emphasize = node is centernode
-               node.draw_node(ax=ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=dy)
+               node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
            
Mbtree.draw_subtree = draw_subtree
```
</details>

上記の修正後に、下記のプログラムを実行すると、実行結果のように、中心となるノードのゲーム盤に赤い外枠が描画されるようになります。ボタンを操作してどのように描画が変化するかを実際に確認してみて下さい。

```python
from tree import Mbtree_GUI
mbtree = Mbtree()
mbtree_gui = Mbtree_GUI(mbtree)
```

実行結果（下記は、深さが 0 と 7 のノードが中心となる場合です）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7497536d-3f63-7af0-018a-b5a0fc17b930.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/799ca83e-7f91-8943-0d34-ab6afda9ace2.png" width="300">

### 深さが 0 のノードが中心となる場合の表示の問題とその原因

気がついていない人が多いかもしれませんが、ノードの強調表示には一つ問題があります。それは、下図のように、深さが 0 のノードが中心となる場合は、__左側の外枠__ が左下図のように、他の枠と比べて __細く描画されてしまう__ というものです。他の深さのノードの場合は、右下図のようにそのような現象はおきません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/dd618072-c2aa-d6d2-3e3d-41a00ab4dbb1.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/c3ea84c4-34b6-0e96-faf1-258dae23eec8.png" width="200">

この問題は、深さが 0 のノードの __左側の外枠の半分__ が、__Axes の描画範囲の外にある事__ が原因です。わかりづらいと思いますので、別の具体例を挙げて説明します。

下記は、__Axes の表示範囲__ の x、y 座標をそれぞれ __0 ~ 1.5 に設定__ し、下記のような __左下の頂点が (0, 0)__ で __幅と高さが 1__  の __正方形の図形の外枠を描画__ するプログラムです。

* 太さが 20 の赤色の外枠を描画する
* その後に、太さが 1 の黒色の外枠を描画する

このプログラムを実行すると、実行結果のように、左と下の外枠の太さが、右と上の外枠と比べて __半分の太さで描画される__ ように見えます。

```python
fig, ax = plt.subplots(figsize=(5, 5))
ax.set_xlim(0, 1.5)
ax.set_ylim(0, 1.5)
frame1 = patches.Rectangle(xy=(0, 0), width=1, height=1, ec="red", fill=False, lw=20)
ax.add_patch(frame1)
frame2 = patches.Rectangle(xy=(0, 0), width=1, height=1, ec="black", fill=False, lw=1)
ax.add_patch(frame2)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/9473ce96-b3ff-ddd4-1247-2a4023a98ec5.png" width="200">

matplotlib では、図形の __外枠の枠線__ は、その図形の __辺を中心とする線で描画__ されるます。そのため、下図のように、__外枠の枠線が太くなればなるほど__、枠線は __図形の内側と外側の両方にはみ出て描画される__ ようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/84d74b14-292c-f7c4-1974-f1cdb4370411.png" width="200">

上記のプログラムでは、正方形の __左と下の辺__ が __Axes の表示範囲の左と下にぴったりと重なるように描画__ しました。そのため、__枠の半分が Axes の表示範囲の外にはみ出てしまい__、左と下の枠の __太さが半分になってしまいます__。

__深さが 0__ のノードのゲーム盤は Axes の __x 座標が 0 ~ 3 の範囲に描画__ しました。また、[前回の記事](https://qiita.com/ysgeso/items/0335cc42055729d0fac3#figure-%E3%81%AE%E5%A4%A7%E3%81%8D%E3%81%95%E3%81%AE%E8%A8%88%E7%AE%97)で、Mbtree_GUI では __Axes の x 座標の表示範囲を 0 ~ 50 の範囲に設定__ したので、深さ 0 のノードのゲーム盤は、__Axes の左端にぴったりくっつくように描画__ されます。これが、深さが 0 のゲーム盤の左の外枠が細く表示される原因です。

なお、実際に様々なノードを中心とする部分木を描画してみるとわかると思いますが、ゲーム盤が Axes の表示範囲の上下の端にぴったりとくっつくように描画されることはないので、ゲーム盤の上と下の外枠の太さが半分になることはありません。

:::note
ゲーム盤が Axes の表示範囲の上下の端にぴったりとくっつくように描画されない理由は、[以前の記事](https://qiita.com/ysgeso/items/ae8a782334350cb9c95b#%E3%83%90%E3%83%A9%E3%83%B3%E3%82%B9%E3%81%AE%E8%89%AF%E3%81%84%E4%BD%8D%E7%BD%AE%E3%81%B8%E3%81%AE%E3%83%8E%E3%83%BC%E3%83%89%E3%81%AE%E6%8F%8F%E7%94%BB)で、ゲーム木をバランス良く描画するため、ノードを子ノードの一覧の真ん中に表示するようにしたからです。ノードの高さが最低でも 4 であるのに対し、ゲーム盤の高さは 3 なので、ゲーム盤は最低でも Axes の y 座標の表示範囲から (4 - 3) / 2 = 0.5 だけずれた位置に描画されます。
:::


### 深さが 0 のノードが中心となる場合の表示の問題の修正

この問題は __図形の外枠を描画する際__ に、__Axes の表示範囲が図形の辺とぴったりと重ならないようにする__ ことで解決することができます。具体的には、Axes の x 座標の表示範囲の最小値を、例えば -1 などの、0 未満の数に変更します。

ただし、Axes の x 座標の表示範囲を 0 ~ 50 から -1 ~ 50 に変更してしまうと、__Figure のサイズをそれに合わせて変更しなければ__、表示される部分木が __歪んで表示されてしまうようになる__ 点に注意が必要です。わかりづらいかもしれないので具体例を挙げます。

下記のプログラムは、__同じ大きさの 2 つの Figure__ に、__同じ大きさの正方形を描画__ する処理を行うプログラムです。ただし、片方は __Axes の x 座標の表示範囲__ が __0 ~ 1__ で、もう片方は __0 ~ 2__ に設定しています。実行結果からわかるように、__同じ大きさの図形を描画__ しているにも関わらず、表示される __図形の形状が異なって表示__ されます。

```python
fig1, ax1 = plt.subplots(figsize=(3, 3))
ax1.set_xlim(0, 1)  # こちらは x 座標の範囲を 0 ～ 1 に設定する
ax1.set_ylim(0, 1)
frame1 = patches.Rectangle(xy=(0.25, 0.25), width=0.5, height=0.5, ec="red", fill=False, lw=1)
ax1.add_patch(frame1)

fig2, ax2 = plt.subplots(figsize=(3, 3))
ax2.set_xlim(0, 2)  # こちらは x 座標の範囲を 0 ～ 2 に設定する
ax2.set_ylim(0, 1)
frame2 = patches.Rectangle(xy=(0.25, 0.25), width=0.5, height=0.5, ec="red", fill=False, lw=1)
ax2.add_patch(frame2)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/150e7622-9186-bccc-b18a-7f327c5a76dd.png" width="150">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/fb0f4c07-549f-c22c-ad00-12b0b22d9829.png" width="150">

図形を正しい大きさで表示するためには、__Axes の x 座標と y 座標の表示範囲の幅__ と、__Figure の大きさをあわせる__ 必要があります。あわせる方法には Axes の表示範囲をあわせる方法と、Figure の大きさをあわせる方法があります。本記事では Axes の表示範囲をあわせる方法を採用し、Figure の大きさを合わせる方法は、この後のノードで説明します。

Axes の表示範囲を合わせる場合は、__Axes の表示範囲の幅を変えないようにする__ 必要があります。Mbtree_GUI の Axes の場合は、x 座標の表示範囲を __0 ~ 50__ から __-1 ~ 49__ に設定する必要があります。ただし、そのように表示範囲を変更する場合は、__右端のノードの表示__ が、__Axes の表示範囲の外に出ないことを確認する__ 必要があります。

右端に表示される __深さが 9__ のノードのゲーム盤は、Axes の x 座標が __45 ~ 48 の範囲に描画__ されます。また、深さ 9 のノードには __子ノードは存在しない__ ので、__ゲーム盤の右にエッジを表す線を描画する必要はありません__。そのため、Axes の x 座標の表示範囲を __-1 ~ 49__ に変更しても問題がないことがわかります。ピンとこない人は図を書いてみると良いでしょう。

Axes の y 座標の表示範囲は、__`update_gui`__ で設定するので、下記のプログラムのように `update_gui` を修正します。

* __3 行目__：Axes の x 座標の表示範囲を左に 1 だけ左にずらすように修正する

```python
 1  def update_gui(self):
 2      self.ax.clear()
 3      self.ax.set_xlim(-1, self.width - 1)
 4      self.ax.set_ylim(0, self.height)   
 5      self.ax.invert_yaxis()
 6      self.ax.axis("off")   
 7    
 8      maxdepth = min(self.centernode.depth + 1, 9)
 9      self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)
10
11  Mbtree_GUI.update_gui = update_gui
```

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

```python
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    maxdepth = min(self.centernode.depth + 1, 9)
    self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)

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

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

```diff_python
def update_gui(self):
    self.ax.clear()
-   self.ax.set_xlim(0, self.width)
+   self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    maxdepth = min(self.centernode.depth + 1, 9)
    self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)

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

上記の修正後に、下記のプログラムを実行すると、実行結果のように、深さが 0 のノードが中心となる場合の外枠の表示の問題が修正されていることが確認できます。

```python
mbtree_gui = Mbtree_GUI(mbtree)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/c7a6acfc-f600-20a6-6488-af72c87d018c.png" width="200">

また、Axes の x 座標の範囲を左にずらしたので、深さが 9 のノードを中心とした場合の表示がおかしくなっていないかを確認する必要があり、下図のように表示がおかしくならないことが確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/abf8585c-8a32-7d46-9fff-1f1d467052cd.png" width="400">

:::note
本記事では採用しませんが、Figure の幅をあわせる方法を説明します。

まず、Axes の x 座標の表示範囲を -1 ~ 50 に変更するために、下記のプログラムのように `update_gui` を修正します。

* __3 行目__：x 座標の表示範囲を -1 ~ 50 に修正する

```python
1  def update_gui(self):
2      self.ax.clear()
3      self.ax.set_xlim(-1, self.width)
元と同じなので省略
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.centernode.depth <= 4:
        maxdepth = self.centernode.depth + 1
    elif self.centernode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)
    
    disabled = self.centernode.parent is None
    self.set_button_status(self.left_button, disabled=disabled)
    disabled = self.centernode.depth >= 6 or len(self.centernode.children) == 0
    self.set_button_status(self.right_button, disabled=disabled)
    disabled = self.centernode.parent is None or self.centernode.parent.children.index(self.centernode) == 0
    self.set_button_status(self.up_button, disabled=disabled)
    disabled = self.centernode.parent is None or self.centernode.parent.children[-1] is self.centernode
    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.ax.clear()
-   self.ax.set_xlim(0, self.width)
+   self.ax.set_xlim(-1, self.width)
元と同じなので省略

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

Axes の x 座標の表示範囲の幅が 50 から 51 に増えたので、それにあわせて Figure を作成する処理を行う `create_widgets` を下記のプログラムのように修正します。

* __3 行目__：Figure の幅の計算式の `self.width` を `(self.width + 1)` に修正する

```python
1  def create_widgets(self):
元と同じなので省略
2      with plt.ioff():
3          self.fig = plt.figure(figsize=[(self.width + 1) * self.size,
4                                          self.height * self.size])
5          self.ax = self.fig.add_axes([0, 0, 1, 1])
元と同じなので省略
6    
7  Mbtree_GUI.create_widgets = create_widgets
```

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

```python
def create_widgets(self):
    self.left_button = self.create_button("←", 100)
    self.up_button = self.create_button("↑", 100)
    self.right_button = self.create_button("→", 100)
    self.down_button = self.create_button("↓", 100)
    
    with plt.ioff():
        self.fig = plt.figure(figsize=[(self.width + 1) * 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
```
</details>

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

```diff_python
def create_widgets(self):
元と同じなので省略
    with plt.ioff():
-       self.fig = plt.figure(figsize=[self.width * self.size,
-                                       self.height * self.size])
+       self.fig = plt.figure(figsize=[(self.width + 1) * self.size,
+                                       self.height * self.size])
        self.ax = self.fig.add_axes([0, 0, 1, 1])
元と同じなので省略
    
Mbtree_GUI.create_widgets = create_widgets
```
</details>

本記事が Figure の大きさをあわせる方法を採用しなかったのは、`create_widgets` の修正が必要になるからです。ただし、どちらの方法でも問題は修正できるので、どちらの修正方法を採用してかまいません。
:::

:::note
先程の例では、Axes の x 座標の表示範囲を 0 ~ 1 から 0 ~ 2 に変更しました。この場合は、表示範囲の幅が 1 から 2 に 2 倍の変化が生じるので、Figure の大きさを変えないと、表示される図形の形が大きく変化してしまいます。

一方、Axes の x 座標の表示範囲を 0 ~ 50 から -1 ~ 50 の範囲に変更した場合は、表示範囲の幅は 50 から 51 になるため、全体の 5 % しか増えません。そのため、Figure の大きさを変更しなくても、表示の見た目がほとんど変わらないので、Figure の大きさを変更しないという方法も考えられます。
:::

## 部分木の深さの改良

[前回の記事](https://qiita.com/ysgeso/items/0335cc42055729d0fac3#figure-%E3%81%AE%E5%A4%A7%E3%81%8D%E3%81%95%E3%81%AE%E8%A8%88%E7%AE%97)では、__部分木の高さが大きくなりすぎないようにする__ ために、__部分木の深さ__ を中心となるノードの __子ノードまでの深さ__ にすることにしました。

確かにそうすることで部分木の高さを減らすことができましたが、できれば __もっと深いノードまで部分木を表示したほうが__、ゲーム木の __全体像を把握しやすく__ なります。

そこで、中心となるノードからの深さを増やした場合に、部分木の高さがいくつになるかを計算し、__部分木の高さがあまり高くならない範囲のゲーム木の深さを調べる__ ことにします。

### 孫ノードまでの深さの部分木の高さの計算

まず、中心となるノードより __2 つ深い__、__孫ノードまでの部分木__ の高さの最大値を計算します。下図は、[前回の記事](https://qiita.com/ysgeso/items/0335cc42055729d0fac3#figure-%E3%81%AE%E5%A4%A7%E3%81%8D%E3%81%95%E3%81%AE%E8%A8%88%E7%AE%97)で子ノードまでの部分木の高さを計算する際に使った図です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b4985b36-c1c0-4cc0-f5d8-b7962cdd1912.png" width="400">

中心となるノードからの __部分木の深さが変化__ した場合は、上図の __赤枠の高さは変化__ しますが、__緑枠の高さは変化しません__。そこで、__孫ノードまでの部分木__ を描画する場合に、赤枠の中に最大でいくつゲーム盤が縦に表示されるかを計算することにします。

赤枠の部分に __最も多くのゲーム盤が縦に並ぶ__ のは、`centernode` の __すべての子ノードが子ノードを持つ場合__ です。例えば、`centernode` の子ノードが決着がついた局面であれば、そのノードには子ノードがないので、その分だけ縦に並ぶゲーム盤の数が少なくなります。

下図は、`centernode` のすべての子ノードに子ノードが存在する場合の部分木の一例で、下記のプログラムを実行することで表示することができます[^2]。このプログラムでは、深さ 6 のノードを中心とし、深さ 8 の孫ノードまでを表示しています。

```python
mbtree.draw_subtree(mbtree.nodelist_by_depth[6][7], maxdepth=8)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/42cb6616-e1a4-bd32-f95e-3b3d9277dc3c.png" width="400">

上図の場合は、深さ 6 の孫ノードの数は「深さ 6 の子ノードの数 * 深さ 7 の子ノードの数」という式で計算することができます。深さ d の子ノードの数は __9 - d__ で計算できるので、中心となるノードの深さが d の場合は、深さ d の __孫ノードの数の最大値__ は (9 - d) * (9 - (d + 1)) = __(9 - d) * (8 - d)__ という式で計算できます。ただし、ゲーム木の深さは 9 までしかないので、深さ 8 の場合は 9 - 8 = 1、深さ 9 の場合は 1 になる点に注意が必要です。

[前回の記事](https://qiita.com/ysgeso/items/0335cc42055729d0fac3#figure-%E3%81%AE%E5%A4%A7%E3%81%8D%E3%81%95%E3%81%AE%E8%A8%88%E7%AE%97)と同様に、緑枠の中に縦に並ぶゲーム盤の数の最大値は、赤枠の中の最大値 + 兄弟ノードの数 になるので、以下のようになります。

| d | 赤枠の最大値 | 兄弟ノードの数 | 最大値 | 
|:-:|:-:|:-:|:-:|
| 0  | 9 * 8 = 72  | 0 | 72 | 
| 1 ~ 7  | (9 - d) * (8 - d)  | 9 - d | (9 - d) * (9 - d)[^3] |
| 8  | 1  | 9 - 8 = 1 | 2 |
| 9 | 1 | 9 - 9 = 0 | 1 |

0 から 9 までのそれぞれの深さの最大値を計算すると以下のようになります。表の 1 行目は、[前回の記事](https://qiita.com/ysgeso/items/0335cc42055729d0fac3#figure-%E3%81%AE%E5%A4%A7%E3%81%8D%E3%81%95%E3%81%AE%E8%A8%88%E7%AE%97)で計算した `centernode` の子ノードまでの部分木の、2 行目は `centernode` の孫ノードまでの部分木の最大値を表します。

| `d` | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| 子ノードまでの部分木の最大値 | 9 | 16 | 14  | 12  | 10  | 8  | 6  | 4  | 2 | 1 |
| 孫ノードまでの部分木の最大値 | 72 | 64 | 49  |36  | 25  | 16  | 9  | 4  | 2 | 1 |

上記の表から、`centernode` の __孫ノードまでの部分木を描画する場合__ は、`centernode` の __深さが 5 以上の場合__ の最大値は、__子ノードまでの部分木の最大値である 16 以下__ になることがわかります。従って、__深さ 5 以上__ のノードを中心とする部分木を描画する場合は、__孫ノードまでの部分木を描画__ しても、__Axes の表示範囲内に収まる__ ことになります。

同様の方法で、`centernode` より __3 つ深い__、__ひ孫のノードまでの部分木__ の高さの最大値を計算することにします。この場合は、赤い枠線の部分に縦に描画されるノードの最大値は、`centernode` の深さが d の場合は、先程と同様の考え方で __(9 - d) * (8 - d) * (7 - d)__ になるので、それぞれの深さの最大値は下記の表のようになります。ただし、深さが 4 以下のノードの場合は、明らかに 16 を超えるので計算を省略します。

| d | 赤枠の最大値 | 兄弟ノードの数 | 最大値 | 
|:-:|:-:|:-:|:-:|
| 5  | 4 * 3 * 2 = 24  | 9 - 5 = 4 | 28 |
| 6  | 3 * 2 * 1 = 6  | 9 - 6 = 3 | 9 |
| 7  | 2 * 1 = 2  | 9 - 7 = 2 | 4 |
| 8  | 1  | 9 - 8 = 1 | 2 |
| 9 | 1 | 9 - 9 = 0 | 1 |

下記は、0 から 9 までのそれぞれの深さの最大値の表に、ひ孫までの部分木の最大値を加えたものです。表から、`centernode` の __ひ孫ノードまでの部分木を描画__ する場合は、`centernode` の __深さが 6 以上の場合の最大値__ は、子ノードまでの部分木の最大値である __16 以下になる__ ことがわかります。

| `d` | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| 子ノードまでの部分木の最大値 | 9 | 16 | 14  | 12  | 10  | 8  | 6  | 4  | 2 | 1 |
| 孫ノードまでの部分木の最大値 | 72 | 64 | 49  |36  | 25  | 16  | 9  | 4  | 2 | 1 |
| ひ孫ノードまでの部分木の最大値 |  |  |   |  |   |  28   | 9  | 4  | 2 | 1 |

__深さ 6 のひ孫ノードの深さ__ はゲーム木の深さの __最大値である 9__ なので、上記から、以下の事がわかります。

* `centernode` の深さが 4 以下の場合は、子ノードまでの部分木の最大値は 16 
* `centernode` の深さが 5 場合は、孫ノードまでの部分木の最大値は 16 
* `centernode` の深さが 6 以上の場合は、深さ 9 までの部分木の最大値は 9 

従って、__下記の深さのノードまで部分木を描画する__ ようにしても、Axes の表示範囲内に表示が収まることがわかります。

| `centernode` の深さ | 部分木の深さ |
|:--|:--|
| 4 以下  | 子ノード  |
| 5  | 孫ノード  |
| 6 以上  | 深さ 9 までのノード  |

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

* __2 ~ 7 行目__：`centernode` の深さに応じた `maxdepth` を計算する

```python
 1  def update_gui(self):
元と同じなので省略   
 2      if self.centernode.depth <= 4:
 3          maxdepth = self.centernode.depth + 1
 4      elif self.centernode.depth == 5:
 5          maxdepth = 7
 6      else:
 7          maxdepth = 9
 8      self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)
 9
10  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.centernode.depth <= 4:
        maxdepth = self.centernode.depth + 1
    elif self.centernode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)

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

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

```diff_python
def update_gui(self):
元と同じなので省略
-   maxdepth = min(self.centernode.depth + 1, 9)
+   if self.centernode.depth <= 4:
+       maxdepth = self.centernode.depth + 1
+   elif self.centernode.depth == 5:
+       maxdepth = 7
+   else:
+       maxdepth = 9
    self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)

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

上記の修正後に、下記のプログラムを実行すると、実行結果のように、深さが 5 のノードが中心となる場合は孫ノードまで、深さ 6 のノードが中心となる場合は深さ 9 のノードまでの部分木が描画されるようになります。

```python
mbtree_gui = Mbtree_GUI(mbtree)
```

実行結果（中心となるノードが深さ 5 と 6 の場合の図です）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/43c7e6ab-b181-b3f4-3621-6fddc7a8abae.png" width="300">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7e8a9e64-362f-f8ba-51db-3280c3d0857c.png" width="300">

## 中心となるノードの移動可能な範囲の改良

下記は、中心となるノードがの深さが 6 の場合と 7 の場合の図です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b008f9ff-c07a-cce3-ce15-58f0ff40bef8.png" width="300">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/57683663-5a64-3053-2575-85c200a3006f.png" width="300">

よく見ると、右図は、左図の __赤枠の部分の図と同じ内容__ になっていることがわかります。このように、__深さが 7 以上__ のノードを中心とする __部分木__ は、そのノードの __深さ 6 の親ノードを中心とする部分木の一部__ になってしまうので、ゲーム木の __全体像を把握するという目的__ において、わざわざ __深さ 7 以上の部分木を描画__ することに __意味はありません__。

そこで、ボタンの操作によって、__深さが 7 以上__ のノードを中心とする __部分木を表示しない__ ように修正することにします。

下記は、そのように `create_event_handler` を修正したプログラムです。__深さが 7 以上になる__ ような操作は、__→ ボタン__ をクリックして子ノードに移動する場合 __だけ__ なので、__→ ボタン以外のイベントハンドラを修正する必要はありません__。

* __3 行目__：→ ボタンをクリックした時のイベントハンドラで `centernode` を移動する条件式に、`centernode` の深さが 6 以下であるという条件を追加する

```python
1  def create_event_handler(self):
元と同じなので省略
2      def on_right_button_clicked(b=None):
3          if self.centernode.depth < 6 and len(self.centernode.children) > 0:
4              self.centernode = self.centernode.children[0]
5              self.update_gui()
元と同じなので省略
6
7  Mbtree_GUI.create_event_handler = create_event_handler
```

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

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

    def on_up_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if index > 0:
                self.centernode = self.centernode.parent.children[index - 1]
                self.update_gui()
            
    def on_down_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if self.centernode.parent.children[-1] is not self.centernode:
                self.centernode = self.centernode.parent.children[index + 1]
                self.update_gui()            
            
    self.left_button.on_click(on_left_button_clicked)
    self.right_button.on_click(on_right_button_clicked)
    self.up_button.on_click(on_up_button_clicked)
    self.down_button.on_click(on_down_button_clicked)

    def on_key_press(event):
        keymap = {
            "left": 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]()
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)

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

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

```diff_python
def create_event_handler(self):
元と同じなので省略
    def on_right_button_clicked(b=None):
        if self.centernode.depth < 6 and len(self.centernode.children) > 0:
            self.centernode = self.centernode.children[0]
            self.update_gui()
元と同じなので省略

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

実行結果は省略しますが、上記の修正後に、下記のプログラムを実行すると、→ キーで深さが 7 以上のノードに移動できなくなることを確認して下さい。

```python
mbtree_gui = Mbtree_GUI(mbtree)
```

## ボタンの状態の変更

現状では、ボタンの色が常に緑色で表示されるようになっている点がわかりづらいので、クリックしても中心となるノードを __移動できない場合はボタンを灰色で表示して操作できない__ ように修正することにします。

ボタンの色を変更して操作できないようするメソッドは、GUI クラスの [__`set_button_status`__](https://qiita.com/ysgeso/items/c2d569b1be171fbe4b31#set_button_status-%E3%81%A8-update_widgets_status-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%AE%9A%E7%BE%A9) メソッドとして定義しましたので、それを利用することができます。

### GUI クラスの修正

[以前の記事](https://qiita.com/ysgeso/items/2acd8c5ceb8e9cda7233#gui-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E5%AE%9A%E7%BE%A9)で、__ウィジェットの状態を更新__ する __`update_widgets_status`__ という[__抽象メソッド__](https://qiita.com/ysgeso/items/2acd8c5ceb8e9cda7233#%E6%8A%BD%E8%B1%A1%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E6%8A%BD%E8%B1%A1%E3%82%AF%E3%83%A9%E3%82%B9)を __GUI クラスに定義し__ ましたが、よく考えると __ウィジェットの状態の更新__ は、__GUI で必須となる処理ではありません__。例えば、常に操作を行うことができるボタンしか存在しない GUI では、ウィジェットの状態を変更する必要はありません。

そこで、下記のプログラムのように、__`update_widgets_status`__ という抽象クラスを __削除した GUI クラスを定義し直す__ ことにします。なお、`update_widget_status` メソッドの定義を削除するだけなので、行番号や、修正箇所は省略します。

<details><summary><strong>GUI クラスの定義（長いのでクリックして表示して下さい）</strong></summary>

```python
from abc import ABCMeta, abstractmethod
import matplotlib as mlp
import ipywidgets as widgets

class GUI(metaclass=ABCMeta):
    """GUI の処理を行うクラスの基底クラス."""

    def __init__(self):
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')
        
        self.disable_shortcutkeys()
        self.create_widgets()
        self.create_event_handler()
        self.display_widgets() 
        self.update_gui()
       
    @abstractmethod
    def create_widgets(self):
        """ウィジェットを作成する."""        
        pass
    
    @abstractmethod
    def create_event_handler(self):
        """イベントハンドラを定義する."""        
        pass
    
    @abstractmethod
    def display_widgets(self):
        """ウィジェットを配置して表示する."""
        pass

    @abstractmethod
    def update_gui(self):
        """GUI の表示を更新する."""
        pass

    @staticmethod
    def disable_shortcutkeys():
        """matplotlib の Figure のデフォルトのショートカットキー操作を禁止する."""
        
        attrs = [ "fullscreen", "home", "back", "forward", "pan", "zoom", "save", "help",
                "quit", "quit_all", "grid", "grid_minor", "yscale", "xscale", "copy"]
        for attr in attrs:
            mlp.rcParams[f"keymap.{attr}"] = []     
            
    @staticmethod
    def create_button(description:str, width:float):   
        """ボタンのウィジェットを作成する.
            
        Args:        
            description:
                ボタンに表示する文字列
            width:
                ボタンの横幅
        """
        
        return widgets.Button(
            description=description,
            layout=widgets.Layout(width=f"{width}px"),
            style={"button_color": "lightgreen"},
        )   
        
    @staticmethod
    def set_button_status(button, disabled:bool):   
        """ ボタンのウィジェットの状態を設定する
    
        Args:
            button:
                ボタンのウィジェット
            disabled:
                False の場合は緑色で表示し、操作できるようにする
                True の場合は灰色で表示し、操作できないようにする
        """
        
        button.disabled = disabled
        button.style.button_color = "lightgray" if disabled else "lightgreen"   
```
</details>

### `update_gui` の修正

Marubatsu_GUI では、`update_gui` で行う処理が長かったので、ウィジェットの状態の更新を行う処理 `update_widgets_status` というメソッドを定義し、`update_gui` から呼び出していましたが、Mbtree_GUI の `update_gui` で行う処理は短いので、その中に下記のプログラムのようにボタンの状態を更新する処理を直接記述することにします。

* __2、4、6、8__：それぞれのボタンが __操作できない場合に `True` になる条件式__ を記述し、`disalbed` に代入する。`create_event_handler` の中のボタンのイベントハンドラの中に記述する条件式を記述すれば良いと思う人がいるかもしれないが、それらはボタンを __操作できる場合に `True` になる条件式__ なので、__その逆を記述する必要がある__
* __3、5、7、9__：`set_button_status` メソッドを利用して、4 つのボタンの状態を設定する

```python
 1  def update_gui(self):
元と同じなので省略   
 2      disabled = self.centernode.parent is None
 3      self.set_button_status(self.left_button, disabled=disabled)
 4      disabled = self.centernode.depth >= 6 or len(self.centernode.children) == 0
 5      self.set_button_status(self.right_button, disabled=disabled)
 6      disabled = self.centernode.parent is None or self.centernode.parent.children.index(self.centernode) == 0
 7      self.set_button_status(self.up_button, disabled=disabled)
 8      disabled = self.centernode.parent is None or self.centernode.parent.children[-1] is self.centernode
 9      self.set_button_status(self.down_button, disabled=disabled)
10
11  Mbtree_GUI.update_gui = update_gui
```

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

```python
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    if self.centernode.depth <= 4:
        maxdepth = self.centernode.depth + 1
    elif self.centernode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)
    
    disabled = self.centernode.parent is None
    self.set_button_status(self.left_button, disabled=disabled)
    disabled = self.centernode.depth >= 6 or len(self.centernode.children) == 0
    self.set_button_status(self.right_button, disabled=disabled)
    disabled = self.centernode.parent is None or self.centernode.parent.children.index(self.centernode) == 0
    self.set_button_status(self.up_button, disabled=disabled)
    disabled = self.centernode.parent is None or self.centernode.parent.children[-1] is self.centernode
    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):
元と同じなので省略   
+   disabled = self.centernode.parent is None
+   self.set_button_status(self.left_button, disabled=disabled)
+   disabled = self.centernode.depth >= 6 or len(self.centernode.children) == 0
+   self.set_button_status(self.right_button, disabled=disabled)
+   disabled = self.centernode.parent is None or self.centernode.parent.children.index(self.centernode) == 0
+   self.set_button_status(self.up_button, disabled=disabled)
+   disabled = self.centernode.parent is None or self.centernode.parent.children[-1] is self.centernode
+   self.set_button_status(self.down_button, disabled=disabled)

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

上記の修正後に、下記のプログラムを実行すると、実行結果のように、操作できないボタンが灰色で描画されるようになります。中心となるノードを移動して、ボタンの色が正しく表示されるようになったことを確認して下さい。

```python
mbtree_gui = Mbtree_GUI(mbtree)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/6c6a13ba-24f1-a1cc-6832-139df953e46c.png" width="300">

# 今回の記事のまとめ

今回の記事では、ゲーム木を表示する GUI の改良をいくつか行いました。次回の記事では別の改良を行う予定です。

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

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

# 次回の記事

https://qiita.com/ysgeso/items/7b687d25a4ab300f5865

[^1]:emphasize は、__強調する__ という意味の英単語です
[^2]:`mbtree.nodelist_by_depth[6][0]` から順番に、そのような部分木が表示されるまで `draw_subtree` を実行して見つけました
[^3]:$x × y + x × z = x × (y + z)$ という公式を使うことで、$(9 - d) × (8 - d) + (9 - d)$ は、下記のように計算できます<br>$(9 - d) × (8 - d) + (9 - d)$<br>$= (9 - d) × (8 - d) + (9 - d) × 1$<br>$= (9 - d) × (8 - d + 1)$<br>$= (9 - d) * (9 - d)$