Pythonで〇×ゲームのAIを一から作成する　その96　テンキーとマウスによる中心となるノードの移動

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/17b0438676f4a41a906c

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

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

| リンク | 説明 |
|:--|:--|
| [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 の 4 つのボタンが下図のように並んでいますが、これではわかりづらいので、ゲーム機のコントローラーの十字キーのような配置にすることにします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0ae393e0-2e90-6a86-a368-80689e724862.png" width="300">

具体的には、下図のように配置します。なお、上図ではボタンの幅を 100 ピクセルにしましたが、下図のように配置した場合にボタンの幅を 100 ピクセルにするとバランスが少々悪く見えたので、下図ではボタンの幅を 50 ピクセルにしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/44210b52-8dba-2a33-775a-2933194dc324.png" width="100">

## ボタンの配置の方法

上図のようにボタンを配置する方法としては、これまで通り HBox と VBox を組み合わせる方法と、格子（grid）状に区切られれた表のようなものの中にウィジェットを配置することができる GridBox というウィジェットを利用する方法が考えられます。

GridBox は柔軟な配置を行うことができて便利なのですが、使いこなすためにはいろいろと学ばなければならないことがあるので、本記事では HBox と VBox を組み合わせる方法を紹介します。GridBox の使い方に興味がある方は、下記のリンク先を参照して下さい。

https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Layout.html#the-grid-layout

## Label ウィジェットを利用した、大きさを持つ空白のウィジェットの作成

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/9682a7c6-224d-b33a-5238-b8012f8ebce6.png" width="150">

上図のようなボタンの配置は、下記のような __3 つの HBox を作成__ し、それらを __VBox で縦に並べて配置す__ ることで、行うことができます。なお、下記の「50 px」は、表示の幅が 50 ピクセルであるという意味を表します

* 「50 px の空白、50 px の ↑ ボタン、50 px の空白」を並べた Hbox
* 「50 px の ← ボタン、50 px の空白、50 px の → ボタン」を並べた Hbox
* 「50 px の空白、50 px の ↓ ボタン、50 px の空白」を並べた Hbox

50 px の __空白__ は、__文字列を表示する Label__ というウィジェットを利用することで表示することができます。Lable ウィジェットは、ipywidgets の __`Label`__ という関数を使って、下記のようなプログラムで作成することができます。実行結果のように、キーワード引数 `value` で指定した文字列が表示された Label が作成されます。

```python
import ipywidgets as widgets

label = widgets.Label(value="ラベル")
display(label)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3df7a48f-fb3a-2639-6d7b-ff46baf76e74.png" width="50">

Label に表示する文字列を __空文字__（`""`）にすることで、__何も表示しない__、__横幅のある__ 空白のラベルを表示することができます。また、Label には、他のウィジェットと同様にキーワード属性 [`layout`](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#layout) を使って __表示幅などを設定__ することができるので、下記のプログラムによって、幅が 50 ピクセルの空白のラベルを作成することができます。

```python
widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
```

Label の詳細については、下記のリンク先を参照して下さい。

https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#label

## `create_widgets` の修正

まず、__`create_widgets`__ で、幅が 50 ピクセルの空白の Label を作成します。下図のボタンの配置を行うためには、__5 つの空白の Label__ を HBox の中に配置する必要がありますが、その 5 つの Label は __いずれも幅が同じ__ で、__後から Label に文字を表示することはない__ ので、Label を __一つだけ作成__ し、__5 箇所に同じ Label を配置__ すれば済みます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/44210b52-8dba-2a33-775a-2933194dc324.png" width="100">

:::note
後からいずれかの Label の表示内容を変更する必要がある場合は、5 つの異なる Label を作成する必要があります。同じ Label を 5 箇所に配置してしまうと、__5 つの Label の表示内容が同時に変化してしまう__ からです。
:::

下記は、そのように `create_widgets` を修正したプログラムです。なお、先程説明したように、ボタンの幅を 50 ピクセルに修正しました。

* __5 ~ 8 行目__：4 つのボタンの幅を 50 ピクセルに修正する
* __9 行目__：幅が 50 ピクセルの空白の Label を作成し、`label` 属性に代入する
  
```python
 1  from tree import Mbtree_GUI
 2  import matplotlib.pyplot as plt
 3
 4  def create_widgets(self):
 5      self.left_button = self.create_button("←", 50)
 6      self.up_button = self.create_button("↑", 50)
 7      self.right_button = self.create_button("→", 50)
 8      self.down_button = self.create_button("↓", 50)
 9      self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
元と同じなので省略
10    
11  Mbtree_GUI.create_widgets = create_widgets
```

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

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

def create_widgets(self):
    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.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
```
</details>

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

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

def create_widgets(self):
-   self.left_button = self.create_button("←", 100)
+   self.left_button = self.create_button("←", 50)
-   self.up_button = self.create_button("↑", 100)
+   self.up_button = self.create_button("↑", 50)
-   self.right_button = self.create_button("→", 100)
+   self.right_button = self.create_button("→", 50)
-   self.down_button = self.create_button("↓", 100)
+   self.down_button = self.create_button("↓", 50)
+   self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
元と同じなので省略
    
Mbtree_GUI.create_widgets = create_widgets
```
</details>

## `display_widgets` の修正

次に、`display_widgets` を下記のプログラムのように修正し、4 つのボタンが十字の形に配置されるようにします。先ほど説明したように、5 箇所にある __空白の Label のウィジェット__ は、__同じウィジェットを使いまわしています__。

* __2 ~ 4 行目__：先ほど説明したようにウィジェットを配置した 3 行分の HBox を作成する
* __5 行目__：3 つの HBox と Figure を縦に並べた VBox を作成し、`display` で表示する

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

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

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

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

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

上記の修正後に、下記のプログラムを実行することで、実行結果のように 4 つのボタンが十字の形に配置されて表示されるようになります。また、4 つのボタンが正しく動作することを、ボタンを操作して確認して下さい。

```python
from tree import Mbtree

mbtree = Mbtree()
mbtree_gui = Mbtree_GUI(mbtree)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/9d153b14-b4dc-a152-c9a2-548909fcb020.png" width="150">

# 任意の子ノードへの移動

現状では、→ ボタンで子ノードへ移動する際に、__先頭の子ノードにしか移動できない__ ので、例えば最後の子ノードに移動する場合は、→ ボタンをクリックした後で、何度も ↓ ボタンをクリックしなければならない点が不便です。

そこで、__任意の子ノードへ移動できる__ ように修正することにします。どのような UI でそのような操作を行うかについて少し考えてみて下さい。

## テンキーを使った任意の子ノードへの移動

〇×ゲームのゲーム盤は 3 x 3 の 9 マスで、テンキーの 1 ~ 9 のボタンに対応させることができます。そのため、〇×ゲームの GUI では、[以前の記事](https://qiita.com/ysgeso/items/17c74178449148ad3ef7#%E3%82%AD%E3%83%BC%E5%85%A5%E5%8A%9B%E3%81%AB%E3%82%88%E3%82%8B%E6%93%8D%E4%BD%9C)で説明したように、テンキーを使って、対応するゲーム盤のマスに着手を行うことができるようにしました。

そこで、ゲーム木を視覚化する GUI でも、__テンキーを使って任意の子ノードへ移動する__ という操作方法が考えられます。具体的には、以下のような操作方法です。

* 現在表示されている部分木の中心となるノードに対して、__テンキーに対応するマスに着手__ を行った場合の子ノードへ移動する
* ただし、ゲームの決着がついている場合や、既にマークが配置されているマスに対応するテンキーを押した場合は、何も行わない

### Node クラスの修正

上記のように Mbtree_GUI クラスを改良するためには、中心となるノードの __それぞれの子ノード__ の局面の、__直前に行われた着手を知る必要__ が生じます。

直前に行われた着手は、Marubatsu クラスのインスタンスの __`last_move`__ 属性に代入されているので、`centernode` の最初の子ノードの場合は、`centernode.mb.children[0].last_move` に直前の着手が代入されていますが、そのような方法でそれぞれの子ノードを調べるのは、わかりづらく、プログラムの記述が大変です。

そこで、本記事では __dict__ を使って、ノードに対して __行われた着手__ と __子ノードの対応を記録する__ ことにします。具体的には下記のように Node クラスを修正します。

* Node クラスのインスタンスに __`children_by_move`__ という名前の属性を追加する。この名前は、__着手によって対応づけられた子ノード__ の一覧という意味を表す
* その属性には、__キーが着手__ を表し、__キーの値がその着手を行った局面を表す子ノード__ を表す dict を代入する
* ↑、↓ ボタンを押した時に必要となる、前後の兄弟ノードや、最後の子ノードを調べる処理は、list のほうが dict よりやりやすいので、__`children` 属性はそのまま利用する__

まず、Node クラスの `__init__` メソッドを下記のプログラムのように修正します。

* __8 行目__：`children_by_move` 属性を、空の dict で初期化する

```python
 1  from tree import Node
 2
 3  def __init__(self, mb, parent=None, depth=0):
 4      self.mb = mb
 5      self.parent = parent
 6      self.depth = depth
 7      self.children = []
 8      self.children_by_move = {}
 9    
10  Node.__init__ = __init__
```

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

```python
from tree import Node

def __init__(self, mb, parent=None, depth=0):
    self.mb = mb
    self.parent = parent
    self.depth = depth
    self.children = []
    self.children_by_move = {}
    
Node.__init__ = __init__
```
</details>

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

```diff_python
from tree import Node

def __init__(self, mb, parent=None, depth=0):
    self.mb = mb
    self.parent = parent
    self.depth = depth
    self.children = []
+   self.children_by_move = {}
    
Node.__init__ = __init__
```
</details>

次に、子ノードを挿入する __`insert`__ メソッドを下記のプログラムのように修正します。

* __3 行目__：`children_by_move` 属性に対して、挿入する子ノードに対して行われた着手をキーとし、キーの値にその子ノードを代入する処理を追加する

```python
1  def insert(self, node):
2      self.children.append(node)
3      self.children_by_move[node.mb.last_move] = node
4
5  Node.insert = insert
```
<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def insert(self, node):
    self.children.append(node)
    self.children_by_move[node.mb.last_move] = node

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

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

```diff_python
def insert(self, node):
    self.children.append(node)
+   self.children_by_move[node.mb.last_move] = node

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

なお、[以前の記事](https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf#%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E5%8F%AF%E8%83%BD%E3%81%AA%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%A8%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E5%80%A4)で説明したように、__dict のキー__ には、__ハッシュ可能なオブジェクト__ を利用することができ、__`last_move`__ 属性には、下記のプログラムの実行結果のように、__ハッシュ可能なオブジェクトである tuple__ が代入されているので、上記のプログラムは正しく動作します。なお、下記のプログラムでは、深さ 1 のノードの `last_move` 属性を表示しました。

```python
print(mbtree.nodelist_by_depth[1][0].mb.last_move)
```

実行結果

```
(0, 0)
```

Node クラスのインスタンスに __新しい属性を加えた__ ので、下記のプログラムで __Mbtree クラスのインスタンスを作成し直す__ 必要があります。作成し直した後で、ルートノードの `children_by_move` 属性を表示すると、実行結果のようにルートノードに対して行われた着手と、子ノードの対応を記録する dict が代入されていることが確認できます。

なお、dict の内容をわかりやすく表示するために [`pprint`](https://qiita.com/ysgeso/items/44884dcf168aab949508#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93%E3%81%AE%E7%A2%BA%E8%AA%8D) を利用しました。

```python
from pprint import pprint

mbtree = Mbtree()
print(mbtree.root.children_by_move)
```

実行結果

```
ノードの作成に関する表示は省略
{(0, 0): <tree.Node object at 0x000002DC13F4EF50>,
 (0, 1): <tree.Node object at 0x000002DC13F565D0>,
 (0, 2): <tree.Node object at 0x000002DC13E35D50>,
 (1, 0): <tree.Node object at 0x000002DC1381CE50>,
 (1, 1): <tree.Node object at 0x000002DC137966D0>,
 (1, 2): <tree.Node object at 0x000002DC13D70D50>,
 (2, 0): <tree.Node object at 0x000002DC13BAF6D0>,
 (2, 1): <tree.Node object at 0x000002DC13C74590>,
 (2, 2): <tree.Node object at 0x000002DC133AB1D0>}
```

### `create_event_handler` の修正

次に、`create_event_handler` に、__テンキーが押された場合の処理を行うイベントハンドラを定義__ します。まず、__押されたキーに対応するマスの座標を計算__ する必要がありますが、その処理は[以前の記事](https://qiita.com/ysgeso/items/b8c4948504e7d11a75d4#%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%86%E3%82%AD%E3%83%BC%E3%81%AE%E5%BA%A7%E6%A8%99%E3%81%AE%E8%A8%88%E7%AE%97%E6%96%B9%E6%B3%95)で説明したように、押されたテンキーの番号が `num` に代入されていた場合は、下記の式で計算できます。

```python
x = num % 3
y = 2 - (num // 3)
```

また、テンキーが押された場合の処理は、`Mbtree_GUI` クラスの `create_event_handler` に記述されている下記のプログラムと同様の方法で記述できます。

```python
 1  if event.key in keymap:
 2      keymap[event.key]()1  try:
 3  else:
 4      try:
 5          num = int(event.key) - 1
 6          event.inaxes = True
 7          event.xdata = num % 3
 8          event.ydata = 2 - (num // 3)
 9          on_mouse_down(event)
10      except:
11          pass
```

なお、上記のプログラムでは、ゲーム盤の上でマスを押した際に呼び出される `on_mouse_down` を利用して着手を行うために、`event.xdata` と `event.ydata` にマスの x、y 座標を計算して代入していますが、今回はそのような処理は必要ありません。

また、4、10 行目の `try` と `except` は、テンキー以外のキーが押された場合に 5 行目の `int(event.key)` の処理でエラーが発生する点に対処するためのものです。忘れた方は、[以前の記事](https://qiita.com/ysgeso/items/b8c4948504e7d11a75d4#%E6%95%B0%E5%80%A4%E4%BB%A5%E5%A4%96%E3%81%AE%E3%82%AD%E3%83%BC%E3%82%92%E6%8A%BC%E3%81%97%E3%81%9F%E5%A0%B4%E5%90%88%E3%81%AE%E5%AF%BE%E5%87%A6)を復習して下さい。

従って、`create_event_handler` は、下記のプログラムのように修正します。`try`、`except` の行に関する説明は、上記で説明したので省略します。

* __7 ~ 9 行目__：押されたテンキーに対応するマスの x、y 座標を計算する
* __10 行目__：着手を表す tuple を計算し、`move` に代入する
* __11 行目__：`move` が `centernode` の `children_by_move` 属性に代入された dict のキーの中に存在するかどうかを [`in` 演算子](https://qiita.com/ysgeso/items/1a8a4319ef3c896ef271#in-%E6%BC%94%E7%AE%97%E5%AD%90%E3%81%A8-not-in-%E6%BC%94%E7%AE%97%E5%AD%90)で判定する。なお、dict に対する in 演算子は、指定した値が __dict のキー__ に存在するかどうかを判定する
* __12 行目__：存在する場合は、そのキーの値に、キーの着手を行った局面のノードが代入されているので、`centernode` にそのノードを代入する
* __13 行目__：中心となるノードが変更されたので、GUI の表示を更新する

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_key_press(event):
元と同じなので省略
 3          if event.key in keymap:
 4              keymap[event.key]()
 5          else:
 6              try:
 7                  num = int(event.key) - 1
 8                  x = num % 3
 9                  y = 2 - (num // 3)
10                  move = (x, y)
11                  if move in self.centernode.children_by_move:
12                      self.centernode = self.centernode.children_by_move[move]
13                      self.update_gui()
14              except:
15                  pass            
16            
17      # fig の画像イベントハンドラを結び付ける
18      self.fig.canvas.mpl_connect("key_press_event", on_key_press)
19
20  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]()
        else:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.centernode.children_by_move:
                    self.centernode = self.centernode.children_by_move[move]
                    self.update_gui()
            except:
                pass            
            
    # 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_key_press(event):
元と同じなので省略
        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.centernode.children_by_move:
+                   self.centernode = self.centernode.children_by_move[move]
+                   self.update_gui()
+           except:
+               pass            
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)

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

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、テンキーで任意の子ノードに移動できるようになったことを確認して下さい。なお、キー操作を行う場合は、[以前の記事](https://qiita.com/ysgeso/items/17c74178449148ad3ef7#figure-%E3%82%92%E9%81%B8%E6%8A%9E%E7%8A%B6%E6%85%8B%E3%81%AB%E3%81%99%E3%82%8B%E5%BF%85%E8%A6%81%E3%81%8C%E3%81%82%E3%82%8B)で説明したように、Figure の上でマウスをクリックして、Figure を選択状態にする必要がある点に注意して下さい。

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

### バグの修正と改良

上記のプログラムでテンキーの操作を行うと、下記のような問題がある事がわかります。

* __親ノードに移動__ したい場合に押す必要がある __← キー__ が、テンキーから離れた場所にあるので __押しづらい__
* [前回の記事](https://qiita.com/ysgeso/items/17b0438676f4a41a906c#%E4%B8%AD%E5%BF%83%E3%81%A8%E3%81%AA%E3%82%8B%E3%83%8E%E3%83%BC%E3%83%89%E3%81%AE%E7%A7%BB%E5%8B%95%E5%8F%AF%E8%83%BD%E3%81%AA%E7%AF%84%E5%9B%B2%E3%81%AE%E6%94%B9%E8%89%AF)で移動できないようにした、__深さが 7 以上のノードに移動できてしまう__

前者の問題を解決する方法として、本記事ではテンキーの中にある、__0 キーで親ノードに移動__ できるようにすることにします。

下記は、上記の 2 つを修正したプログラムです。

* __5 行目__：0 キーを押した場合に `on_left_button_clicked` が呼び出されるように、`keymap` にその情報を追加する
* __12 行目__：`centernode` の深さが 6 未満の場合のみテンキーの操作が行えるようにする

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_key_press(event):
 3          keymap = {
 4              "left": on_left_button_clicked,
 5              "0": on_left_button_clicked,
 6              "right": on_right_button_clicked,
 7              "up": on_up_button_clicked,
 8              "down": on_down_button_clicked,
 9          }
10          if event.key in keymap:
11              keymap[event.key]()
12          elif self.centernode.depth < 6:
13              try:
元と同じなので省略
14
15  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,
            "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]()
        elif self.centernode.depth < 6:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.centernode.children_by_move:
                    self.centernode = self.centernode.children_by_move[move]
                    self.update_gui()
            except:
                pass            
            
    # 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_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:
+       elif self.centernode.depth < 6:
            try:
元と同じなので省略

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

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、2 つの問題が修正されたことを確認して下さい。

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

## マウスの操作による中心となるノードの移動

上記では、テンキーによって任意の子ノードに移動できるようにしましたが、画面に表示される __ノードをマウスで押す__ ことで、そのノードに __直接移動できるとさらに便利__ です。どのような処理を記述すれば、そのようなことが行えるようになるかを少し考えてみて下さい。

上記のような操作を行うには、マウスを押した際に、押した __マウスの座標__ が、画面に表示された __どのノード上にあるかを判定する__ 必要があります。

そのためには、画面に表示されているそれぞれの __ノードの表示範囲を記録しておく__ という方法があります。__ノードの表示範囲の表現方法__ について少し考えてみて下さい。

### Rect クラスの定義

ノードはゲーム盤として表示され、__ゲーム盤は正方形の形状で描画__ されます。そのため、ノードの表示範囲は __長方形[^1]を表すデータとして記録__ することができます。また、ゲーム盤の __辺__ は、__x 軸と y 軸に平行__ になります。

__辺が x 軸と y 軸に平行な長方形__ を表すデータは、一般的に長方形の __頂点の中の 1 つの座標__ と、長方形の __幅と高さ__ を使って表現するのが一般的です。また、一般的に 4 つの頂点の中で、__x と y 座標が最も小さい値を持つ頂点が選ばれます__。

長方形のデータは、上記のように 4 つのデータから構成されるので、長方形を表す rectangle の略である __Rect__ という __長方形を表すクラスを定義__ し、そのインスタンスによって表現することにします。

:::note
これは、Mbtree_GUI クラスの __機能を拡張__ する際に、拡張する機能を __別のクラスで定義して利用する__ という __[委譲](https://qiita.com/ysgeso/items/2acd8c5ceb8e9cda7233#%E5%A7%94%E8%AD%B2composition)__ です。長方形を表す Rect クラスは、Mbtree_GUI クラスと __[is-a の関係](https://qiita.com/ysgeso/items/2acd8c5ceb8e9cda7233#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E7%B6%99%E6%89%BF%E3%81%A8-is-ahas-a-%E9%96%A2%E4%BF%82)にない__ ので、__[継承](https://qiita.com/ysgeso/items/2acd8c5ceb8e9cda7233#%E7%B6%99%E6%89%BFinheritance)を使うべきではありません__。
:::

下記は、Rect クラスの定義です。__`__init__`__ メソッドでは、長方形を表す 4 つのデータを代入する __仮引数を用意__ し、それぞれの __仮引数を同じ名前の属性に代入__ しています。

```python
class Rect:
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
```

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

下記のプログラムのように、Rect メソッドの __インスタンスを作成して `print` で表示__ すると、__その中身がわからないような表示__ が行われてしまします。

```python
rect = Rect(0, 0, 10, 20)
print(rect)
```

実行結果

```
<__main__.Rect object at 0x000002DC48040D50>
```

そこで、[以前の記事](https://qiita.com/ysgeso/items/ed1eb1cacf9a3e7dfa29#__str__-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)で説明した __`__str__`__ メソッドを定義する事で、`print` で Rect のインスタンスを表示した際に、__その中身がわかるような内容が表示__ されるようにします。

下記は、`__str__` メソッドの定義で、`print` で表示した際に表示する、__図形の形状__、__頂点の座標__、__幅__、__高さ__ を表す文字列を返すように定義します。

```python
def __str__(self):
    return f"Rectangle ({self.x}, {self.y}) width = {self.width} height = {self.height}"

Rect.__str__ = __str__
```

上記の修正後に、下記のプログラムを実行することで、`print` で Rect の内容がわかるようになったことが確認できます。

```python
print(rect)
```

実行結果

```
Rectangle (0, 0) width = 10 height = 20
```

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

マウスによるノードの移動処理では、__マウスを押した座標__ が、ノードを表す __長方形の内部にあるかどうかを判定する必要__ があります。そこで、Rect クラスに、__指定した座標が長方形の内部にあるか__ どうかを __判定__ する、下記のようなメソッドを定義する事にします。このメソッドをどのように定義すれば良いかについて少し考えてみて下さい。

* __名前__：長方形の内部（inside）あるか（is）どうかを判定するので `is_inside` とする
* __処理__：実引数で指定した座標が、長方形の内部にあるかどうかを判定する
* __入力__：判定する座標を仮引数 x、y に代入する
* __出力__：(x, y) が長方形の内部にある場合は `True`、そうでない場合は `False` を返す

頂点が (x, y)、幅が width、高さが height の長方形の内部にある点は、下図から以下の性質を同時に満たす必要がある事がわかります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0ae325cf-ee02-0658-ba58-26d126463ad7.png" width="300">

* x 座標が、x 以上 かつ x + width 未満である
* y 座標が、y 以上 かつ y + height 未満である

:::note
上記では、長方形の内部の判定の際に良く使われる __x 以上__、__x + width 未満__ を条件としたので、以下のような判定が行われます。

* __左の辺__ の上の点を長方形の __内部とみなす__
* __右の辺__ の上の点を長方形の __内部とみなさない__

__x より大きい__ や、__x + width 以下__ とすることで、左右の辺を長方形の内部とみなすかどうかを変更することができます。

今回の場合は、辺を長方形の内部とみなすかどうかの違いは、ほとんど影響を与えないのでどちらでもかまいませんが、辺が長方形の内部であるかどうかが重要になる場合は、このことを正しく理解して判定を行う必要がある点に注意して下さい。

このことは、y 座標と上下の辺に関しても同様です。
:::

従って、`is_inside` は下記のプログラムのように定義できます。

* __1 行目__：仮引数 `x`、`y` を持つメソッドとして定義する
* __2 行目__：上記の条件を計算する条件式を記述し、その値を return 文で返す

```python
def is_inside(self, x, y):
    return self.x <= x < self.x + self.width and self.y <= y < self.y + self.height

Rect.is_inside = is_inside
```

下記のプログラムで、頂点が (0, 0)、幅が 10 高さが 20 の長方形を表す `rect` の中に、(5, 5) は 入っているが、(20, 10) は入っていないことが確認できます。興味がある方は、他の座標でもうまく判定できるかを確認してみて下さい。

```python
print(rect.is_inside(5, 5))
print(rect.is_inside(20, 10))
```

実行結果

```
True
False
```

### `draw_node` の修正

次に、Rect クラスを使って、__部分木の各ノードの表示範囲を記録する__ ことにします。そのためには、それぞれの __ノードの表示範囲を記録するための属性__ が必要になるので、その属性の名前を __`nodes_by_rect`__ と命名することにします。

__`nodes_by_rect`__ には、__ノードの表示範囲__ と __そのノードを対応づける dict を代入__ します。具体的には、__キーがノードの表示範囲__ を表し、__キーの値がそのノード__ を表す dict を代入します。これは、先ほどの __`children_by_move` と同様の考え方__ で作られた dict です。

なお、__自作のクラスのインスタンス__ は __ハッシュ可能なオブジェクト__[^2]なので、Rect クラスのインスタンスを dict のキーとして利用することができます。

__ノード__ は、Node クラスの __`draw_node` で表示する__ ので、__`draw_node` の処理を行う際に表示範囲を知る__ ことができます。ただし、__`draw_node`__ は、ゲーム木の視覚化を行う __以外の場合でも呼び出して利用できる__ ので、__`draw_node` の中__ でゲーム木のノードの表示範囲を表す __`nodes_by_rect` 属性の値を変更するべきではありません__。そこで、__`draw_node` の外で表示範囲を利用できる__ ようにするために、下記のプログラムのように、`draw_node` が描画したノードの __表示範囲を表す Rect を返り値として返す__ ように修正することにします。

* __7 行目__：ゲーム盤を描画した後で、ゲーム盤の描画範囲を表す Rect クラスのインスタンスを作成し、`rect` に代入する
* __11 行目__：上記の `rect` を返り値として返すようにする

```python
 1  from marubatsu import Marubatsu_GUI
 2
 3  def draw_node(self, ax=None, maxdepth=None, emphasize=False, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
 4      # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
 5      y = dy + (self.height - 3) / 2
 6      Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, emphasize=emphasize, lw=lw, dx=dx, dy=y)
 7      rect = Rect(dx, y, 3, 3)
元と同じなので省略           
 8          else:
 9              plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
10
11      return rect
12
13  Node.draw_node = draw_node
```

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

```python
from marubatsu import Marubatsu_GUI

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)
    rect = Rect(dx, y, 3, 3)
    # 子ノードが存在する場合に、エッジの線と子ノードを描画する
    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)
            
    return rect

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

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

```diff_python
from marubatsu import Marubatsu_GUI

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, emphasize=emphasize, lw=lw, dx=dx, dy=y)
+   rect = Rect(dx, y, 3, 3)
元と同じなので省略           
        else:
            plt.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>

:::note
7 行目を `rect = Rect(dx, dy, 3, 3)` としないように注意して下さい。筆者は最初はそのように記述してしまい、後で間違いに気づきました。
:::

### `draw_subtree` の修正

部分木は、`draw_subtree` メソッドで描画するので、`draw_subtree` を下記のプログラムのように修正します。

* __2 行目__：これから部分木を新しく描画するので、`nodes_by_rect` 属性に空の dict を代入して初期化する
* __3、5、7、9 行目__：ノードの表示範囲を表す `draw_node` の返り値を `rect` に代入する
* __4、6、8、10 行目__：`node_by_rect` に代入された dict に、`rect` をキーとし、そのキーの値に描画したノードを代入する処理を追加する

```python
 1  def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):   
 2      self.nodes_by_rect = {}
元と同じなので省略
 3                  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
 4                  self.nodes_by_rect[rect] = node
元と同じなので省略
 5                  rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
 6                  self.nodes_by_rect[rect] = sibling
元と同じなので省略
 7          rect = parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
 8          self.nodes_by_rect[rect] = parent
元と同じなので省略
 9              rect = node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
10              self.nodes_by_rect[rect] = node
11            
12  Mbtree.draw_subtree = draw_subtree
```

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

```python
def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):   
    self.nodes_by_rect = {}

    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
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, 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
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        rect = parent.draw_node(ax, maxdepth=maxdepth, 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
            rect = node.draw_node(ax, maxdepth=node.depth, 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, ax=None, size=0.25, lw=0.8, maxdepth=2):   
+   self.nodes_by_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, size=size, lw=lw, dx=dx, dy=dy)
+               self.nodes_by_rect[rect] = node
元と同じなので省略
-               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, lw=lw, dx=dx, dy=dy)
+               self.nodes_by_rect[rect] = sibling
元と同じなので省略
-       parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
+       rect = parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
+       self.nodes_by_rect[rect] = parent
元と同じなので省略
-           node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, y=0)
+           rect = node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, y=0)
+           self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
```
</details>

`draw_subtree` メソッドを呼び出すことで、__`nodes_by_rect` 属性に正しく値が代入されるかどうかを確認__ することにします。まず、下記のプログラムで、ルートノードを中心とする、深さ 1 までの部分木を `draw_subtree` メソッドで描画します。

```python
mbtree.draw_subtree(mbtree.root, maxdepth=1)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/78f1932a-6c38-ad7c-a33d-0d91416000c2.png" width="100">

次に、下記のプログラムで、`nodes_by_rect` のそれぞれの __キー__ と、キーの値が表す __ノードの局面__ を __表示__ します。[`items()`](https://qiita.com/ysgeso/items/44884dcf168aab949508#for-%E6%96%87%E3%81%A7-dict-%E3%81%AE%E3%82%AD%E3%83%BC%E3%81%A8%E5%80%A4%E3%81%AE%E4%B8%A1%E6%96%B9%E3%82%92%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%99%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%97)によって、dict のキーとキーの値の両方を繰り返し処理で得ることができます。実行結果から、上図で表示されたノードが表示される長方形の範囲と、対応するノードが正しく `nodes_by_rect` に記録されていることが確認できます。

```python
for rect, node in mbtree.nodes_by_rect.items():
    print(rect)
    print(node.mb)
```

実行結果

```
Rectangle (0, 16.5) width = 3 height = 3
Turn o
...
...
...

Rectangle (5, 0.5) width = 3 height = 3
Turn x
O..
...
...

Rectangle (5, 4.5) width = 3 height = 3
Turn x
.O.
...
...

Rectangle (5, 8.5) width = 3 height = 3
Turn x
..O
...
...

Rectangle (5, 12.5) width = 3 height = 3
Turn x
...
O..
...

Rectangle (5, 16.5) width = 3 height = 3
Turn x
...
.O.
...

Rectangle (5, 20.5) width = 3 height = 3
Turn x
...
..O
...

Rectangle (5, 24.5) width = 3 height = 3
Turn x
...
...
O..

Rectangle (5, 28.5) width = 3 height = 3
Turn x
...
...
.O.

Rectangle (5, 32.5) width = 3 height = 3
Turn x
...
...
..O
```

### `create_event_handler` の修正

それぞれのノードの表示範囲が記録できたので、__`create_event_handler`__ を修正して、マウスを押した場所にノードが描画されていた場合に、そのノードに移動する処理を記述します。

具体的には、マウスを押した座標が `nodes_by_rect` に記録した __長方形の範囲の中にあるか__ どうかを __順番に調べ__、__中にある事が判明したノードへ移動する__ という処理を行います。

マウスが Figure の上で押された場合に実行するイベントハンドラと Figure の結び付けは、[以前の記事](https://qiita.com/ysgeso/items/1920c68f5a1e8a998796#mpl_connect-%E3%81%AE%E8%A8%98%E8%BF%B0%E6%96%B9%E6%B3%95)で説明したように、下記のプログラムで行います。

```python
self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
```

従って、`create_event_handler` を下記のように修正します。

* __2 行目__：マウスを押した時に実行するイベントハンドラを定義する。マウスを押した場所の座標は、`event.xdata` と `event.ydata` に代入される
* __3 行目__：`nodes_by_rect` からキーとキーの値を順番に取り出す繰り返し処理を行う
* __4 ~ 7 行目__：`node` の深さが 6 以下で、マウスを押した場所が `rect` の内部にある場合は、部分木の中心なるノードそのノードをノードに変更し、GUI の描画の更新処理を行う。残りの繰り返し処理を行う必要はないので、break によって繰り返し処理を終了する
* __11 行目__：Figure とマウスを押した時に実行するイベントハンドラを結び付ける

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_mouse_down(event):
 3          for rect, node in self.mbtree.nodes_by_rect.items():
 4              if node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
 5                  self.centernode = node
 6                  self.update_gui()
 7                  break               
 8           
 9      # fig の画像イベントハンドラを結び付ける
10      self.fig.canvas.mpl_connect("key_press_event", on_key_press)
11      self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    
12
13  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,
            "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]()
        elif self.centernode.depth < 6:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.centernode.children_by_move:
                    self.centernode = self.centernode.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 node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
                self.centernode = node
                self.update_gui()
                break               
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    

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

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

```diff_python
def create_event_handler(self):
元と同じなので省略
+   def on_mouse_down(event):
+       for rect, node in self.mbtree.nodes_by_rect.items():
+           if node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
+               self.centernode = node
+               self.update_gui()
+               break               
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)
+   self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    

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

実行結果は省略しますが、下記のプログラムを実行し、深さが 6 以下のノードの上でマウスを押すと、そのノードを中心とする部分木が表示されることを確認して下さい。

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

# 操作説明の表示

Marubatsu_GUI では、？ ボタンをクリックした際に、操作説明を表示するようにしましたので、同様のボタンを用意し、操作説明を表示することにします。

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

操作説明を表示する [Output](https://qiita.com/ysgeso/items/5941cc59c3e0e173c5cf#output-%E3%82%A6%E3%82%A3%E3%82%B8%E3%82%A7%E3%83%83%E3%83%88%E3%81%AB%E3%82%88%E3%82%8B%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%A8%E6%B6%88%E5%8E%BB) ウィジェットと、？ ボタンのウィジェットを作成する必要があります。下記は、そのように `create_widgets` メソッドを修正したプログラムです。

* __2 行目__：Output ウィジェットを作成し、`output` 属性に代入する
* __7 行目__：ヘルプボタンのウィジェットを作成し、`help_button` 属性に代入する

```python
 1  def create_widgets(self):
 2      self.output = widgets.Output()  
 3      self.left_button = self.create_button("←", 50)
 4      self.up_button = self.create_button("↑", 50)
 5      self.right_button = self.create_button("→", 50)
 6      self.down_button = self.create_button("↓", 50)
 7      self.help_button = self.create_button("？", 50)
 8      self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
元と同じなので省略
 9
10  Mbtree_GUI.create_widgets = create_widgets
```

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

```python
def create_widgets(self):
    self.output = widgets.Output()  
    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
```
</details>

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

```diff_python
def create_widgets(self):
+   self.output = widgets.Output()
    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"))
元と同じなので省略

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

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

Marubatsu_GUI では、操作説明をゲーム盤の下に表示しましたが、Mbtree_GUI では部分木を描画する Figure が縦にかなり長いので、一番上に操作説明を表示するウィジェットを配置することにします。また、？ ボタンは、→ ボタンの右に配置することにします。その際に、→ ボタンと少し離して表示するために、間に空白のラベルを表示することにします。下記はそのように __`display_widgets`__ メソッドを修正したプログラムです。

* __4 行目__：→ ボタンの右に、空白のラベルと ？ ボタンを配置する
* __6 行目__：VBox の先頭に Output ウィジェットを配置するようにする

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

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

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

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

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

上記の修正後に下記のプログラムを実行すると、実行結果のように ？ ボタンが表示されるようになります。なお、実行結果は上半分だけを表示します。

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

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a867027f-d505-9197-ca53-b83f43b67f51.png" width="200">

### `create_event_handler` の修正

次に、`create_event_handler` を下記のプログラムのように修正することで、？ ボタンをクリックした際に操作説明が表示されるようにします。

* __2 ~ 15 行目__：? ボタンをクリックした際に実行するイベントハンドラを定義する
* __3 ~ 15 行目__：[`with self.output`](https://qiita.com/ysgeso/items/5941cc59c3e0e173c5cf#output-%E3%81%B8%E3%81%AE%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%A8%E6%B6%88%E5%8E%BB%E3%81%AE%E5%87%A6%E7%90%86%E3%81%AE%E5%AE%9F%E8%A3%85)のブロックの中に `print` を記述することで、Output ウィジェット内にメッセージを表示する。なお、？ ボタンを何度もクリックした際に、操作説明が何度も表示されないようにするために、3 行目で Output の表示をクリアする
* __21 行目__：？ ボタンとイベントハンドラを結び付ける

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_help_button_clicked(b=None):
 3         self.output.clear_output()
 4         with self.output:
 5             print("""操作説明
 6
 7  下記のキーとボタンで中心となるノードを移動できる。ただし、深さが 7 以上のノードへは移動できない
 8  、0 キー：親ノードへ移動
 9  ↑：一つ前の兄弟ノードへ移動
10  ↓：一つ後の兄弟ノードへ移動
11  →：先頭の子ノードへ移動
12
13  テンキーで、対応するマスに着手が行われた子ノードへ移動する
14  ノードの上でマウスを押すことでそのノードへ移動する
15  """)
16                           
17      self.left_button.on_click(on_left_button_clicked)
18      self.right_button.on_click(on_right_button_clicked)
19      self.up_button.on_click(on_up_button_clicked)
20      self.down_button.on_click(on_down_button_clicked)
21      self.help_button.on_click(on_help_button_clicked)
元と同じなので省略
22
23  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()            
                
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

下記のキーとボタンで中心となるノードを移動できる。ただし、深さが 7 以上のノードへは移動できない
←、0 キー：親ノードへ移動
↑：一つ前の兄弟ノードへ移動
↓：一つ後の兄弟ノードへ移動
→：先頭の子ノードへ移動

テンキーで、対応するマスに着手が行われた子ノードへ移動する
ノードの上でマウスを押すことでそのノードへ移動する
""")
                           
    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]()
        elif self.centernode.depth < 6:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.centernode.children_by_move:
                    self.centernode = self.centernode.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 node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
                self.centernode = node
                self.update_gui()
                break               
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    

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

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

```diff_python
def create_event_handler(self):
元と同じなので省略
+   def on_help_button_clicked(b=None):
+       self.output.clear_output()
+       with self.output:
+           print("""操作説明
+
+下記のキーとボタンで中心となるノードを移動できる。ただし、深さが 7 以上のノードへは移動できない
+、0 キー：親ノードへ移動
+↑：一つ前の兄弟ノードへ移動
+↓：一つ後の兄弟ノードへ移動
+→：先頭の子ノードへ移動
+
+テンキーで、対応するマスに着手が行われた子ノードへ移動する
+ノードの上でマウスを押すことでそのノードへ移動する
+""")
                           
    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)
元と同じなので省略

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

上記の修正後に下記のプログラムを実行し、？ ボタンをクリックすると実行結果のように上部に操作説明が表示されるようになります。なお、実行結果は上半分だけを表示します。

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


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/012c4e2e-23aa-1bc3-2cbd-41d797225316.png" width="400">

:::note
本記事では採用しませんが、？ ボタンで操作説明の表示の ON/OFF の切り替えを行えるようにしたい場合は、下記のプログラムのように Mbtree_GUI クラスの `__init__` メソッド内で、操作説明を表示するかどうかを表す仮引数 `show_instruction` に `False` を代入します。

```python
class Mbtree_GUI(GUI):
    def __init__(self, mbtree, size=0.15):   
        self.mbtree = mbtree
        self.show_instruction = False
        self.size = size
        self.width = 50
        self.height = 64
        self.centernode = self.mbtree.root
        super().__init__()
略
```

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

```diff_python
class Mbtree_GUI(GUI):
-   def __init__(self, mbtree, size=0.15):   
+   def __init__(self, mbtree, size=0.15):   
        self.mbtree = mbtree
+       self.show_instruction = False
        self.size = size
        self.width = 50
        self.height = 64
        self.centernode = self.mbtree.root
        super().__init__()
```
</details>

イベントハンドラの`on_help_button_clicked` では、下記のプログラムの 3 行目のように、`show_instruction` 属性の値を not 演算子で反転し、4 行目で True の場合のみ操作説明を表示するように修正します。

```python
1  def on_help_button_clicked(b=None):
2      self.output.clear_output()
3      self.show_instruction = not self.show_instruction
4      if self.show_instruction:
5          with self.output:
6              print("""操作説明
元と同じなので省略
```

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

```diff_python
def on_help_button_clicked(b=None):
    self.output.clear_output()
+   self.show_instruction = not self.show_instruction
+   if self.show_instruction:
+       with self.output:
+           print("""操作説明
元と同じなので省略
```
</details>

他には、操作説明を削除するボタンを別に用意するという方法も考えられます。
:::

# 今回の記事のまとめ

今回の記事では、テンキーとマウスの操作によって、より柔軟にノードを移動できるように修正しました。また、？ ボタンを配置して操作説明を表示できるように修正しました。

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

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

# 次回の記事

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

[^1]:正方形は、長方形の中の特別な形状です
[^2]:オブジェクトの ID がハッシュ値になります