Pythonで〇×ゲームのAIを一から作成する　その145　Mbtee_Anim での選択中のノードに対するフレームの移動

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/289c8067774951433941

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

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

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

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

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

# 選択中のノードに対するフレームの移動

今回の記事も前回の記事に引き続き Mbtree_Anim による αβ 法の評価値の計算手順のアニメーションの改良を行います。現状では __特定のノード__ の __評価値の計算手順__ を、その __子ノードの評価値だけに注目しながら確認するのが困難__ であるという欠点があります。

わかりづらいと思いますので、具体例を挙げて説明します。例えば、下図の 6 フレーム目の赤枠のノードの __子ノードの評価値が計算される__ のは、その下の図の __9、21、25 フレーム目__ で、赤枠のノードの __評価値が確定__ するのは一番下の図の __27 フレーム目__ です。そのため、__それらのフレームを表示する__ ためには、__求める情報が表示されているか__ どうかを __確認しながら「>」ボタンを何度もクリック__ する必要があります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3d86de51-7d85-edc2-0ae4-5eac3ea4d49f.png" width="500">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/02edafe2-43e8-381a-921e-7417ad60f587.png" width="500">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/461f38cd-f6bd-c576-0d2f-4d4150f57bc3.png" width="500">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/ffce6e2c-4477-112b-c7bc-1e6b11d2c960.png" width="500">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d56e7990-8798-01ea-0702-698c951a3595.png" width="500">

また、上図のような深さが深いノードの場合は「>」ボタンを何度かクリックするだけで済みますが、__深さが浅いノードの場合__ は、次に求める情報が表示される __フレームがかなり先__ になるため、__「>」ボタンでそのようなフレームを表示__ することは __非常に困難__ です。

実際に、最も浅いルートノードの場合は下図のように __最初__ の子ノードの評価値が計算されるのが __9351 フレーム目__、__その次__ の子ノードの評価値が計算されるのが __14327 フレーム目__ なので、これらのフレームを「>」ボタンだけで探し出すのは __現実的ではありません__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3b751a8e-376e-b062-c3ec-a66f1e5aacf9.png" width="350">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/bd6b24c8-1969-d99a-c9cc-674b73832ab5.png" width="350">

この問題を解決するために、Mbtree_Anim の上部に __以下のような操作を行うためのボタンを追加__ することにします。

* __選択中のノード__ の __処理が開始されたフレームに移動__ する
* 選択中のノードの、__前の子ノードの評価値が計算されたフレーム__ に移動する。前の子ノードが存在しない場合は処理が開始されたフレームに移動する
* 選択中のノードの、__次の子ノードの評価値が計算されたフレーム__ に移動する。次の子ノードが存在しない場合は評価値が確定したフレームに移動する
* 選択中のノードの __評価値が確定したフレーム__ に移動する

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

まず、下記のプログラムのように __`create_widgets`__ メソッドを修正して __上記のボタンを作成する__ ようにします。__ボタンの表示__ はそれぞれ __<<、<、>、>>__ にし、__ボタンの説明__ を表示する __ラベルを作成__ しました。

* __11 行目__：αβ 法での評価値の計算の手順のアニメーションであるかどうかは 7 行目で判定しており、その場合は `abfig` 属性に `None` ではない値が代入されるので、`abfig` 属性が `None` ではない場合にボタンなどを作成するようにする
* __12 行目__：ボタンの説明を表示する [Label ウィジェット](https://qiita.com/ysgeso/items/7b687d25a4ab300f5865#label-%E3%82%A6%E3%82%A3%E3%82%B8%E3%82%A7%E3%83%83%E3%83%88%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E5%A4%A7%E3%81%8D%E3%81%95%E3%82%92%E6%8C%81%E3%81%A4%E7%A9%BA%E7%99%BD%E3%81%AE%E3%82%A6%E3%82%A3%E3%82%B8%E3%82%A7%E3%83%83%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90) を作成する
* __13 ~ 16__：4 つのボタンを作成する

```python
 1  import ipywidgets as widgets
 2  import matplotlib.pyplot as plt
 3  from tree import Mbtree_Anim
 4  
 5  def create_widgets(self):
元と同じなので省略
 6      with plt.ioff():
元と同じなので省略
 7          if self.isscore and hasattr(self.mbtree, "ablist_by_score"):
 8              self.abfig = plt.figure(figsize=(5, 1))
元と同じなので省略            
 9          else:
10              self.abfig = None
元と同じなので省略            
11      if self.abfig is not None:
12          self.node_label = widgets.Label("選択中のノード内の移動")
13          self.node_first_button = self.create_button("<<", width=40)
14          self.node_prev_button = self.create_button("<", width=30)
15          self.node_next_button = self.create_button(">", width=30)
16          self.node_last_button = self.create_button(">>", width=40)
17  
18  Mbtree_Anim.create_widgets = create_widgets
```

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

```python
import ipywidgets as widgets
import matplotlib.pyplot as plt
from tree import Mbtree_Anim

def create_widgets(self):
    self.play = widgets.Play(max=self.nodenum - 1, interval=500)
    self.prev_button = self.create_button("<", width=30)
    self.next_button = self.create_button(">", width=30)
    self.frame_slider = widgets.IntSlider(max=self.nodenum - 1, description="frame")
    self.interval_slider = widgets.IntSlider(value=500, min=1, max=2000, description="interval")
    widgets.jslink((self.play, "value"), (self.frame_slider, "value"))    
    widgets.jslink((self.play, "interval"), (self.interval_slider, "value"))

    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 
        if self.isscore and hasattr(self.mbtree, "ablist_by_score"):
            self.abfig = plt.figure(figsize=(5, 1))
            self.abax = self.abfig.add_axes([0, 0, 1, 1])
            self.abfig.canvas.toolbar_visible = False
            self.abfig.canvas.header_visible = False
            self.abfig.canvas.footer_visible = False
            self.abfig.canvas.resizable = False 
        else:
            self.abfig = None
            
    if self.abfig is not None:
        self.node_label = widgets.Label("選択中のノード内の移動")
        self.node_first_button = self.create_button("<<", width=40)
        self.node_prev_button = self.create_button("<", width=30)
        self.node_next_button = self.create_button(">", width=30)
        self.node_last_button = self.create_button(">>", width=40)

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

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

```diff_python
import ipywidgets as widgets
import matplotlib.pyplot as plt
from tree import Mbtree_Anim

def create_widgets(self):
元と同じなので省略
    with plt.ioff():
元と同じなので省略
        if self.isscore and hasattr(self.mbtree, "ablist_by_score"):
            self.abfig = plt.figure(figsize=(5, 1))
元と同じなので省略            
        else:
            self.abfig = None
元と同じなので省略            
+   if self.abfig is not None:
+       self.node_label = widgets.Label("選択中のノード内の移動")
+       self.node_first_button = self.create_button("<<", width=40)
+       self.node_prev_button = self.create_button("<", width=30)
+       self.node_next_button = self.create_button(">", width=30)
+       self.node_last_button = self.create_button(">>", width=40)

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

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

次に、下記のプログラムのように __`display_widgets`__ メソッドを修正して作成したボタンとラベルを表示するようにします。

* __6 ~ 8 行目__：`abfig` 属性の値が `None` でない場合に先程作成したラベルとボタンを横に並べた Hbox を作成し、それを表示するように修正する

```python
 1  def display_widgets(self):
 2      hbox = widgets.HBox([self.play, self.prev_button, self.next_button, self.frame_slider, self.interval_slider])
 3      if self.abfig is None:
 4          display(widgets.VBox([hbox, self.fig.canvas])) 
 5      else:
 6          hbox2 = widgets.HBox([self.node_label, self.node_first_button, self.node_prev_button,
 7                                self.node_next_button, self.node_last_button])
 8          display(widgets.VBox([hbox, hbox2, self.abfig.canvas, self.fig.canvas])) 
 9         
10  Mbtree_Anim.display_widgets = display_widgets
```

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

```python
def display_widgets(self):
    hbox = widgets.HBox([self.play, self.prev_button, self.next_button, self.frame_slider, self.interval_slider])
    if self.abfig is None:
        display(widgets.VBox([hbox, self.fig.canvas])) 
    else:
        hbox2 = widgets.HBox([self.node_label, self.node_first_button, self.node_prev_button,
                              self.node_next_button, self.node_last_button])
        display(widgets.VBox([hbox, hbox2, self.abfig.canvas, self.fig.canvas])) 
        
Mbtree_Anim.display_widgets = display_widgets
```
</details>

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

```diff_python
def display_widgets(self):
    hbox = widgets.HBox([self.play, self.prev_button, self.next_button, self.frame_slider, self.interval_slider])
    if self.abfig is None:
        display(widgets.VBox([hbox, self.fig.canvas])) 
    else:
-       display(widgets.VBox([hbox, self.abfig.canvas, self.fig.canvas])) 
+       hbox2 = widgets.HBox([self.node_label, self.node_first_button, self.node_prev_button,
+                             self.node_next_button, self.node_last_button])
+       display(widgets.VBox([hbox, hbox2, self.abfig.canvas, self.fig.canvas])) 
        
Mbtree_Anim.display_widgets = display_widgets
```
</details>

上記の修正後に下記のプログラムで `calc_score_by_ab` によって αβ 法で評価値を計算した mbtree に対して Mbtree_Anim を実行すると、実行結果のように __作成したラベルとボタンが表示される__ ようになったことが確認できます。

```python
from tree import Mbtree

mbtree = Mbtree(algo="df")
mbtree.calc_score_by_ab(mbtree.root)
Mbtree_Anim(mbtree, isscore=True)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/5fc62902-2335-a782-f51d-760e19b3a9cf.png" width="350">

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

次に、__`create_event_handler`__ の中に __4 つのボタン__ をクリックした際に実行する __イベントハンドラを定義__ する必要があります。それぞれのボタンに対するイベントハンドラにどのようなプログラムを記述すれば良いかについて少し考えてみて下さい。

### 選択中のノードの処理が開始されたフレームの計算方法

既に定義済の、アニメーションの __フレームを 1 つ前に移動__ するボタンの __イベントハンドラ__ は下記のプログラムように定義されています。このプログラムでは、__アニメーションのフレーム__ を表す __`self.play.value` の値を 1 減らし__、__ゲーム木の描画を更新__ するために __`update_gui`__ メソッドを呼び出しています。

```python
def on_prev_button_clicked(b=None):
    self.play.value -= 1
    self.update_gui()
```

選択中のノードの処理が開始されたフレームに移動する __<< ボタン__ をクリックした際に実行する __イベントハンドラ__ は、上記のプログラムを参考に下記のプログラムのように定義できます。下記の「__`self.play.value` に選択中のノードの処理が開始されたフレームの番号を代入する__」の部分にどのようなプログラムを記述すれば良いかについて少し考えてみて下さい。

```python
def on_node_first_button_clicked(b=None):
    self.play.value に選択中のノードの処理が開始されたフレームの番号を代入する
    self.update_gui()
```

[前回の記事](https://qiita.com/ysgeso/items/fd421d90cd1f47e38cdb#mbtree-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE-calc_node_by_ab-%E3%81%AE%E4%BF%AE%E6%AD%A3)でノードに対して行われる処理を下記の表の __4 つに分類__ し、アニメーションのフレームとして下記の __4 種類の処理を行ったフレームのデータを記録__ しました。また、アニメーションの各フレームで行われた処理を記録する __`ablist_by_score`__ の要素の中に __フレームの状態を表す文字列を記録__ しました。

| 行われた処理 | 状態を表す文字列 |
|:--|:--|
| __ノードの処理が開始__  | `"start"` |
| __子ノードの評価値の計算の直後__  |  `"score"` |
| __α 値または β 値に対する処理__  | `"update"` |
| __ノードの処理が終了して評価値が確定__  | `"end"` |

__αβ 法__ では、__ノードに対する処理__ は __下記の順番__ で行われます。なお、__以降の説明__ では __行う処理__ を上記の表の __状態を表す文字列で表記__ します。

1. `"start"` の処理
2. `"score"` と `"update"` の処理
    * 2 の処理は子ノードの数だけ繰り返す
    * ただし、枝狩りが行われた場合は残りの子ノードに対する 2 の処理は省略する
3. `"end"` の処理

上記から、__すべてのノード__ は必ず __`"start"`__ 状態が記録されたフレーム __ではじまり__、__`"end"`__ の状態が記録されたフレーム __で終わる__ ことがわかります。

__選択中のノードの処理が開始__ されたフレームの __状態は `"start"`__ なので、そのフレームは以下のアルゴリズムで探すことができます。

1. __現在のフレーム__ の状態が __`"start"`__ の場合は __そのフレーム__ が求めるフレームである
2. そうでない場合は、現在のフレームから __1 つずつフレームをさかのぼって__ 以下の条件を __すべて満たすフレームが見つかるまで__  チェックし、見つかったフレームが求めるフレームである
    * そのフレームで行われる処理が、__現在のフレームと同じノード__ の処理を行っている
    * そのフレームで行われる __処理の状態__ が __`"start"`__ である

### << ボタンをクリックした際のイベントハンドラの定義

 下記はそのように __<< ボタン__ をクリックした際のイベントハンドラを __`on_node_first_button_clicked`__ という名前で定義したプログラムです。

* __2 行目__：現在のフレームの番号を `frame` に代入する
* __3 行目__：現在のフレームの選択中のノードを `selectednode` に代入する
* __4 行目__：現在のフレームの状態を `selectedstatus` に代入する。なお `ablist_by_score` の要素に代入されている [tuple の内容](https://qiita.com/ysgeso/items/fd421d90cd1f47e38cdb#mbtree-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE-calc_node_by_ab-%E3%81%AE%E4%BF%AE%E6%AD%A3) は `(α 値, β 値, 子ノードの評価値, フレームの状態を表す文字列)` なので、フレームの状態は tuple の 3 番の要素に代入されている
* __5、6 行目__：現在のフレームの状態が `"start"` の場合はフレームの移動を行う必要がないので return 文を実行してイベントハンドラの処理を終了する
* __7 ~ 12 行目__：求めるフレームが見つかるまで無限ループで繰り返し処理を行う。なお、先程説明したように、ノードの処理は __必ず `"start"` の状態で始まる__ ので、求めるフレームは __必ず見つかり__、__無限ループから抜ける__ ことは保証されている
* __8 行目__：`frame` を 1 減らして一つ前のフレームにさかのぼる
* __9、10 行目__：さかのぼったフレームのノードと状態を `node` と `status` に代入する
* __11、12 行目__：`node` が `selectednode` と等しく、`status` が `"start"` であればそれが求めるフレームなので break 文で無限ループを抜ける
* __13、14 行目__：繰り返し処理を抜けた時点の `frame` に求めるフレームの番号が代入されているので、それを `self.play.value` に代入してフレームを移動し、`update_gui` メソッドを呼び出してゲーム木の描画を更新する

```python
 1  def on_node_first_button_clicked(b=None):
 2      frame = self.play.value
 3      selectednode = self.mbtree.nodelist_by_score[frame]
 4      selectedstatus = self.mbtree.ablist_by_score[frame][3]
 5      if selectedstatus == "start":
 6          return
 7      while True:
 8          frame -= 1
 9          node = self.mbtree.nodelist_by_score[frame]
10          status = self.mbtree.ablist_by_score[frame][3]
11          if node == selectednode and status == "start":
12              break
13      self.play.value = frame
14      self.update_gui()
```

### 前の子ノードの評価値が計算されたフレームの計算方法

下記は先ほど示した αβ 法でのノードに対する処理の順番を再掲したものです。

1. `"start"` の処理
2. `"score"` と `"update"` の処理
    * 2 の処理は子ノードの数だけ繰り返す
    * ただし、枝狩りが行われた場合は残りの子ノードに対する 2 の処理は省略する
3. `"end"` の処理

上記から、選択中のノードの前の子ノードの評価値が計算されたフレームが __存在する場合__ は、そのフレームを以下のアルゴリズムで求めることができます。__先ほどのアルゴリズムとの違い__ は、「__現在のフレーム__ が `"start"` であることを __チェックしない__」点と、下記の __太字__ の「__処理の状態__ が `"start"` ではなく、__`"score"`__ であることをチェックする」点です。

* 現在のフレームから 1 つずつフレームをさかのぼって以下の条件をすべて満たすフレームが見つかるまでチェックし、見つかったフレームが求めるフレームである 
    * そのフレームで行われる処理が、現在のフレームと同じノードの処理を行っている
    * そのフレームで行われる処理の状態が __`"score"`__ である

前の子ノードが __存在しない場合__ は __処理が開始されたフレームに移動する__ ので、__先程と同じアルゴリズム__ で求めることができます。

実は、前の子ノードが存在するかどうかで __アルゴリズムを分ける必要はなく__、__チェックする条件__ を「処理の状態が __`"start"` または `"score"`__」のようにすることで、下記のアルゴリズムで __一つにまとめる__ ことができます。その理由について少し考えてみて下さい。

なお、下記の __太字の部分以外__ は、最初の選択中のノードの処理が開始されたフレームを求めるアルゴリズムと __全く同じ__ です。

1. 現在のフレームの状態が `"start"` の場合はそのフレームが求めるフレームである
2. そうでない場合は、現在のフレームから 1 つずつフレームをさかのぼって以下の条件をすべて満たすフレームが見つかるまでチェックし、見つかったフレームが求めるフレームである 
    * そのフレームで行われる処理が、現在のフレームと同じノードの処理を行っている
    * そのフレームで行われる処理の状態が __`"start"` または `"score"`__ である

上記のようにまとめることができる理由は以下の通りです。

* 前の子ノードが __存在する場合__ は、フレームをさかのぼった場合に同じノードで __状態が `"score"`__ になるフレームが、同じノードで __状態が `"start"`__ になるフレーム __よりも前に見つかる__。従って、`"start"` または `"score"` のフレームを探した際に __みつかるのは `"score"` のフレーム__ になる
* 前の子ノードが __存在しない場合__ は、フレームをさかのぼった場合に同じノードで __状態が `"score"`__ になるフレームは __存在しない__。従って、`"start"` または `"score"` のフレームを探した際に __みつかるのは `"start"` のフレーム__ になる

### < ボタンをクリックした際のイベントハンドラの定義

下記はそのように __`on_node_prev_button_clicked`__ を定義したプログラムです。__11 行目__ が変わった __以外__ は __`on_node_first_button_clicked`__ と __全く同じ__ です。

```python
 1  def on_node_first_button_clicked(b=None):
 2      frame = self.play.value
 3      selectednode = self.mbtree.nodelist_by_score[frame]
 4      selectedstatus = self.mbtree.ablist_by_score[frame][3]
 5      if selectedstatus == "start":
 6          return
 7      while True:
 8          frame -= 1
 9          node = self.mbtree.nodelist_by_score[frame]
10          status = self.mbtree.ablist_by_score[frame][3]
11          if node == selectednode and (status == "start" or status == "score"):
12              break
13      self.play.value = frame
14      self.update_gui()
```

<details><summary><strong><code>on_node_first_button_clicked</code>との違い</strong></summary>

```diff_python
-def on_node_first_button_clicked(b=None):
+def on_node_prev_button_clicked(b=None):
    frame = self.play.value
    selectednode = self.mbtree.nodelist_by_score[frame]
    selectedstatus = self.mbtree.ablist_by_score[frame][3]
    if selectedstatus == "start":
        return
    while True:
        frame -= 1
        node = self.mbtree.nodelist_by_score[frame]
        status = self.mbtree.ablist_by_score[frame][3]
-       if node == selectednode and status == "start":
+       if node == selectednode and (status == "start" or status == "score"):
            break
    self.play.value = frame
    self.update_gui()
```
</details>

### 選択中のノードの評価値が確定したフレームの計算方法

__評価値が確定__ したフレームは現在のフレームから __後ろに向かって `"end"` の状態__ のフレームを __探せばよい__ ので、先程と同様の考え方で下記のアルゴリズムで計算できます。選択中のノードの処理が開始されたフレームのアルゴリズムとの違いは __太字__ の「__`"start"` が `"end"` になった__」点と、「フレームをさかのぼるのではなく __後ろに向かって__ に辿る」点だけです。

1. 現在のフレームの状態が __`"end"`__ の場合はそのフレームが求めるフレームである
2. そうでない場合は、現在のフレームから 1 つずつフレームを __後ろに向かって辿って__ 以下の条件をすべて満たすフレームが見つかるまでチェックし、見つかったフレームが求めるフレームである 
    * そのフレームで行われる処理が、現在のフレームと同じノードの処理を行っている
    * そのフレームで行われる処理の状態が __`"end"`__ である

従って、>> ボタンをクリックした際のイベントハンドラは下記のプログラムのように定義できます。

```python
 1  def on_node_last_button_clicked(b=None):
 2      frame = self.play.value
 3      selectednode = self.mbtree.nodelist_by_score[frame]
 4      selectedstatus = self.mbtree.ablist_by_score[frame][3]
 5      if selectedstatus == "end":
 6          return
 7      while True:
 8          frame += 1
 9          node = self.mbtree.nodelist_by_score[frame]
10          status = self.mbtree.ablist_by_score[frame][3]
11          if node == selectednode and status == "end":
12              break
13      self.play.value = frame
14      self.update_gui()
```

<details><summary><strong><code>on_node_first_button_clicked</code>との違い</strong></summary>

```diff_python
-def on_node_first_button_clicked(b=None):
+def on_node_prev_button_clicked(b=None):
    frame = self.play.value
    selectednode = self.mbtree.nodelist_by_score[frame]
    selectedstatus = self.mbtree.ablist_by_score[frame][3]
-   if selectedstatus == "start":
+   if selectedstatus == "end":
        return
    while True:
-       frame -= 1
+       frame += 1
        node = self.mbtree.nodelist_by_score[frame]
        status = self.mbtree.ablist_by_score[frame][3]
-       if node == selectednode and status == "start":
+       if node == selectednode and status == "end":
            break
    self.play.value = frame
    self.update_gui()
```
</details>

### 次の子ノードの評価値が計算されたフレームの計算方法

同様に、次の子ノードの評価値が計算されたフレームは下記のアルゴリズムで計算できます。上記との違いは __太字__ の「__`"end"` または `"score"`__」の部分だけです。 

1. 現在のフレームの状態が `"end"` の場合はそのフレームが求めるフレームである
2. そうでない場合は、現在のフレームから 1 つずつフレームを後ろに向かって辿って以下の条件をすべて満たすフレームが見つかるまでチェックし、見つかったフレームが求めるフレームである 
    * そのフレームで行われる処理が、現在のフレームと同じノードの処理を行っている
    * そのフレームで行われる処理の状態が __`"end"` または `"score"`__ である

従って、> ボタンをクリックした際のイベントハンドラは下記のプログラムのように定義できます。

```python
 1 def on_node_last_button_clicked(b=None):
 2     frame = self.play.value
 3     selectednode = self.mbtree.nodelist_by_score[frame]
 4     selectedstatus = self.mbtree.ablist_by_score[frame][3]
 5     if selectedstatus == "end":
 6         return
 7     while True:
 8         frame += 1
 9         node = self.mbtree.nodelist_by_score[frame]
10         status = self.mbtree.ablist_by_score[frame][3]
11         if node == selectednode and (status == "end" or status =="score"):
12             break
13     self.play.value = frame
14     self.update_gui()
```

<details><summary><strong><code>on_node_last_button_clicked</code>との違い</strong></summary>

```diff_python
-def on_node_last_button_clicked(b=None):
+def on_node_next_button_clicked(b=None):
    frame = self.play.value
    selectednode = self.mbtree.nodelist_by_score[frame]
    selectedstatus = self.mbtree.ablist_by_score[frame][3]
    if selectedstatus == "end":
        return
    while True:
        frame += 1
        node = self.mbtree.nodelist_by_score[frame]
        status = self.mbtree.ablist_by_score[frame][3]
-       if node == selectednode and status == "end":
+       if node == selectednode and (status == "end" or status =="score"):
            break
    self.play.value = frame
    self.update_gui()
```
</details>

### フレームを探して移動する関数の定義

上記で定義した __4 つのイベントハンドラ__ は、__以下の点を除くと全く同じ処理を行う__ ので、__異なる部分__ のデータを __仮引数に代入__ して処理を行う __関数を定義__ することにします。

* 5 行目で __`selectedstatus`__ と比較するデータ
* 8 行目で __`frame`__ に加算する値
* 11 行目で __`status`__ の条件

またその違いは以下の表のようになっています。

|  | << | < | > | >> |
|:-:|:-:|:-:|:-:|:-:|
| __5 行目__  | `"start"`  | `"start"`  | `"end"`  | `"end"`  |
| __8 行目__  | -1  | -1 | +1  | +1  |
| __11 行目__  | `"start"`  | `"start"` または `"score"`  | `"end"` または `"score"`  | `"end"`  |

定義する関数の名前は `change_frame`、それぞれの仮引数の名前は `edge_status`[^1]、`diff`[^2]、`status_list` としました。

__11 行目のデータ__ は __複数存在する場合がある__ ので __list や tuple で表現するのが一般的__[^3]ですが、そのような場合は上記の __11 行目__ のプログラムで __`==` 演算子を利用することはできません__。どのように 11 行目を記述すれば良いかを少し考えてみて下さい。

__ある値が複数の値のいずれかに一致するか__ を判定する際に、11 行目では `==` 演算子と or 演算子を利用していますが、複数の値を `["end", "score"]` のように list などの __[シーケンス型](https://qiita.com/ysgeso/items/6ca248133ff4d59a81d4#%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B9%E5%9E%8B%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B)__ で記述した場合は __in 演算子__ を利用して __`status in ["end", "score"]`__ のように記述できます。従って、`change_frame` は下記のプログラムのように定義できます。`on_node_first_button_clicked` との違いは仮引数と 5、8、11 行目だけです。

```python
 1  def change_frame(edge_status, diff, status_list):
 2      frame = self.play.value
 3      selectednode = self.mbtree.nodelist_by_score[frame]
 4      selectedstatus = self.mbtree.ablist_by_score[frame][3]
 5      if selectedstatus == edge_status:
 6          return
 7      while True:
 8          frame += diff
 9          node = self.mbtree.nodelist_by_score[frame]
10          status = self.mbtree.ablist_by_score[frame][3]
11          if node == selectednode and status in status_list:
12              break
13      self.play.value = frame
14      self.update_gui()
```

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

```python
def change_frame(edge_status, diff, status_list):
    frame = self.play.value
    selectednode = self.mbtree.nodelist_by_score[frame]
    selectedstatus = self.mbtree.ablist_by_score[frame][3]
    if selectedstatus == edge_status:
        return
    while True:
        frame += diff
        node = self.mbtree.nodelist_by_score[frame]
        status = self.mbtree.ablist_by_score[frame][3]
        if node == selectednode and status in status_list:
            break
    self.play.value = frame
    self.update_gui()
```
</details>

<details><summary><strong><code>on_node_first_button_clicked</code>との違い</strong></summary>

```diff_python
-def on_node_first_button_clicked(b=None):
+def change_frame(edge_status, diff, status_list):
    frame = self.play.value
    selectednode = self.mbtree.nodelist_by_score[frame]
    selectedstatus = self.mbtree.ablist_by_score[frame][3]
-   if selectedstatus == "start":
+   if selectedstatus == edge_status:
        return
    while True:
-       frame -= 1
+       frame += diff
        node = self.mbtree.nodelist_by_score[frame]
        status = self.mbtree.ablist_by_score[frame][3]
-       if node == selectednode and status == "start":
+       if node == selectednode and status in status_list:
            break
    self.play.value = frame
    self.update_gui()
```
</details>

### Mbtree_Anim の `create_event_handler`　メソッドの修正

下記は __`change_frame`__ を利用して __4 つのイベントハンドラの定義と登録__ を行うように __`create_event_handler`__ を修正したプログラムです。

* __2 ~ 15 行目__：上記の `change_frame` をローカル関数として定義する
* __17 ~ 27 行目__：4 つのボタンのイベントハンドラを、適切な実引数を記述して `change_frame` を呼び出すように定義する
* __29 ~ 33 行目__：αβ 法による評価値の計算手順を表示することを表す `abfig` 属性が `None` でない場合にボタンとイベントハンドラを結び付ける。なお、利用しないイベントハンドラを定義してもプログラムの動作には影響を与えないので、イベントハンドラの定義を 29 行目の if 文のブロックの中に記述する必要はない

```python
 1 def create_event_handler(self):
元と同じなので省略
 2     def change_frame(edge_status, diff, status_list):
 3         frame = self.play.value
 4         selectednode = self.mbtree.nodelist_by_score[frame]
 5         selectedstatus = self.mbtree.ablist_by_score[frame][3]
 6         if selectedstatus == edge_status:
 7             return
 8         while True:
 9             frame += diff
10             node = self.mbtree.nodelist_by_score[frame]
11             status = self.mbtree.ablist_by_score[frame][3]
12             if node == selectednode and status in status_list:
13                 break
14         self.play.value = frame
15         self.update_gui()
16             
17     def on_node_first_button_clicked(b=None):
18         change_frame("start", -1, ["start"])
19             
20     def on_node_prev_button_clicked(b=None):
21         change_frame("start", -1, ["start", "score"])
22 
23     def on_node_next_button_clicked(b=None):
24         change_frame("end", 1, ["end", "score"])
25         
26     def on_node_last_button_clicked(b=None):
27         change_frame("end", 1, ["end"])
28     
29     if self.abfig is not None:
30         self.node_first_button.on_click(on_node_first_button_clicked)
31         self.node_prev_button.on_click(on_node_prev_button_clicked)
32         self.node_next_button.on_click(on_node_next_button_clicked)
33         self.node_last_button.on_click(on_node_last_button_clicked)
34         
35 Mbtree_Anim.create_event_handler = create_event_handler
```

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

```python
def create_event_handler(self):
    def on_play_changed(changed):
        self.update_gui()
        
    def on_prev_button_clicked(b=None):
        self.play.value -= 1
        self.update_gui()
        
    def on_next_button_clicked(b=None):
        self.play.value += 1
        self.update_gui()

    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)

    self.play.observe(on_play_changed, names="value")
    
    def change_frame(edge_status, diff, status_list):
        frame = self.play.value
        selectednode = self.mbtree.nodelist_by_score[frame]
        selectedstatus = self.mbtree.ablist_by_score[frame][3]
        if selectedstatus == edge_status:
            return
        while True:
            frame += diff
            node = self.mbtree.nodelist_by_score[frame]
            status = self.mbtree.ablist_by_score[frame][3]
            if node == selectednode and status in status_list:
                break
        self.play.value = frame
        self.update_gui()
            
    def on_node_first_button_clicked(b=None):
        change_frame("start", -1, ["start"])
            
    def on_node_prev_button_clicked(b=None):
        change_frame("start", -1, ["start", "score"])

    def on_node_next_button_clicked(b=None):
        change_frame("end", 1, ["end", "score"])
        
    def on_node_last_button_clicked(b=None):
        change_frame("end", 1, ["end"])
    
    if self.abfig is not None:
        self.node_first_button.on_click(on_node_first_button_clicked)
        self.node_prev_button.on_click(on_node_prev_button_clicked)
        self.node_next_button.on_click(on_node_next_button_clicked)
        self.node_last_button.on_click(on_node_last_button_clicked)
        
Mbtree_Anim.create_event_handler = create_event_handler
        
```
</details>

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

```diff_python
def create_event_handler(self):
元と同じなので省略
+   def change_frame(edge_status, diff, status_list):
+       frame = self.play.value
+       selectednode = self.mbtree.nodelist_by_score[frame]
+       selectedstatus = self.mbtree.ablist_by_score[frame][3]
+       if selectedstatus == edge_status:
+           return
+       while True:
+           frame += diff
+           node = self.mbtree.nodelist_by_score[frame]
+           status = self.mbtree.ablist_by_score[frame][3]
+           if node == selectednode and status in status_list:
+               break
+       self.play.value = frame
+       self.update_gui()
            
+   def on_node_first_button_clicked(b=None):
+       change_frame("start", -1, ["start"])
            
+   def on_node_prev_button_clicked(b=None):
+       change_frame("start", -1, ["start", "score"])

+   def on_node_next_button_clicked(b=None):
+       change_frame("end", 1, ["end", "score"])
        
+   def on_node_last_button_clicked(b=None):
+       change_frame("end", 1, ["end"])
    
+   if self.abfig is not None:
+       self.node_first_button.on_click(on_node_first_button_clicked)
+       self.node_prev_button.on_click(on_node_prev_button_clicked)
+       self.node_next_button.on_click(on_node_next_button_clicked)
+       self.node_last_button.on_click(on_node_last_button_clicked)
        
Mbtree_Anim.create_event_handler = create_event_handler
```
</details>

## ボタンの動作の確認

上記の修正後に下記のプログラムを実行し、__4 つのボタンの動作が正しく行われるかどうかを確認__ します。

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

下記はルートノードの処理が開始された最初の __0 番のフレームで > ボタンをクリック__ した場合の図で、ルートノードの __最初の子ノードの評価値が計算された 9351 フレーム目が正しく表示__ されます。また、__子ノードの評価値が計算されたフレーム__ であることを表す __[薄黄色の背景色](https://qiita.com/ysgeso/items/fd421d90cd1f47e38cdb#%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%81%A7%E8%A1%8C%E3%82%8F%E3%82%8C%E3%82%8B%E5%87%A6%E7%90%86%E3%81%AE%E6%98%8E%E7%A2%BA%E5%8C%96)__ で表示されることが確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/25beb582-79c8-66a9-82e5-f8854c40ce7e.png" width="350">

もう一度 __> ボタン__ をクリックすると、下図のように __その次の子ノード__ の評価値が計算された __14327 フレーム目__ が __薄黄色の背景色で表示__ されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/669bb081-d0eb-88e3-7730-bc4877289952.png" width="350">

図は省略しますが、上図で __< ボタン__ をクリックすると __9351 フレーム目__ が、__さらに < ボタン__ をクリックするとルートノードの __処理が開始された 0 フレーム目__ が表示されます。

__&gt;&gt; ボタン__ をクリックするとルートノードの __評価値が確定__ した下図の __73185 フレーム目__ が __[薄紫色の背景色](https://qiita.com/ysgeso/items/fd421d90cd1f47e38cdb#%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%81%A7%E8%A1%8C%E3%82%8F%E3%82%8C%E3%82%8B%E5%87%A6%E7%90%86%E3%81%AE%E6%98%8E%E7%A2%BA%E5%8C%96)__ で表示されます。また、__ルートノードが選択__ されている場合に __> ボタンを何度かクリック__ すると、__最後には下図のフレームが表示__ されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4981082c-3e9c-fc3a-4832-763a60e0f66d.png" width="350">

図は省略しますが、__<< ボタン__ をクリックするか、__何度も < ボタンをクリック__ するとルートノードの __処理が開始__ された __0 フレーム目__ が　__[白色の背景色](https://qiita.com/ysgeso/items/fd421d90cd1f47e38cdb#%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%81%A7%E8%A1%8C%E3%82%8F%E3%82%8C%E3%82%8B%E5%87%A6%E7%90%86%E3%81%AE%E6%98%8E%E7%A2%BA%E5%8C%96)__ で表示されます。

上記ではルートノードが選択された状態で 4 つのボタンが正しく動作するかどうかを確認しまして。余裕がある方は __他のノードが選択された様々なフレーム__ で 4 つのボタンをクリックして、__動作が正しく行われるかどうかを確認__ してみて下さい。

## ボタンの表示に関する改良

ノードの __評価値が確定したフレーム__ では __> ボタン__ や __>> ボタン__ をクリックしても __フレームは移動しません__ が、上図のように > ボタンと >> ボタンが __緑色で表示される__ のでそれらのボタンを押した際にフレームが __移動するように見えてしまうという問題__ があります。同様に __ノードの処理を開始__ したフレームでも下図のようにクリックしてもフレームが移動しない __< ボタンと << ボタン__ が __緑色で表示される__ という問題があります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/6f5bb4e9-c35e-bd44-61c9-b4ddbd984b05.png" width="350">

そこで、ボタンをクリックしても __フレームが移動しない場合__ は、そのボタンを __灰色で表示して操作できないようにする__ という改良を行うことにします。

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

__&lt;&lt; ボタン__ と __&lt; ボタン__ をクリックしても __フレームが移動しない__ のはその __フレームの状態が `"start"`__ の場合です。__>> ボタン__ と __> ボタン__ を押してもフレームが移動しないのはその __フレームの状態が `"end"` の場合__ です。従って、`update_gui` メソッドを下記のプログラムのように修正します。

* __4 行目__：`abfig` 属性が `None` ではない場合に、現在のフレームの状態を `ablist_by_score` から取り出して `status` に代入する
* __5 ~ 7 行目__：[`set_button_status`](https://qiita.com/ysgeso/items/94fba56cb97dc4787ca8#%E3%83%9C%E3%82%BF%E3%83%B3%E3%81%AE%E7%8A%B6%E6%85%8B%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0%E3%81%AE%E5%AE%9A%E7%BE%A9) を利用して、`status` が `"start"` の場合に << ボタンと < ボタンを灰色で表示して操作できないようにする
* __8 ~ 10 行目__：`set_button_status` を利用して、`status` が `"end"` の場合に > ボタンと >> ボタンを灰色で表示して操作できないようにする

```python
 1  def update_gui(self):
元と同じなので省略
 2      if self.abfig is not None:
 3          self.update_ab()
 4          status = self.mbtree.ablist_by_score[self.play.value][3]
 5          disabled = status == "start"
 6          self.set_button_status(self.node_first_button, disabled=disabled) 
 7          self.set_button_status(self.node_prev_button, disabled=disabled)
 8          disabled = status == "end"
 9          self.set_button_status(self.node_next_button, disabled=disabled)  
10          self.set_button_status(self.node_last_button, disabled=disabled)
11 
12      disabled = self.play.value == 0
13      self.set_button_status(self.prev_button, disabled=disabled)
14      disabled = self.play.value == self.nodenum - 1
15      self.set_button_status(self.next_button, disabled=disabled)
16     
17  Mbtree_Anim.update_gui = update_gui
```

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

```python
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(-1, self.height - 1)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    self.selectednode = self.nodelist[self.play.value]
    self.centernode = self.selectednode
    if self.mbtree.algo == "bf":
        if self.centernode.depth > 0:
            self.centernode = self.centernode.parent
    while self.centernode.depth > 6:
        self.centernode = self.centernode.parent
    if self.centernode.depth <= 4:
        maxdepth = self.centernode.depth + 1
    elif self.centernode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
                            anim_frame=self.play.value, isscore=self.isscore, 
                            ax=self.ax, maxdepth=maxdepth, size=self.size)
    if self.abfig is not None:
        self.update_ab()
        status = self.mbtree.ablist_by_score[self.play.value][3]
        disabled = status == "start"
        self.set_button_status(self.node_first_button, disabled=disabled)                           
        self.set_button_status(self.node_prev_button, disabled=disabled)
        disabled = status == "end"
        self.set_button_status(self.node_next_button, disabled=disabled)                           
        self.set_button_status(self.node_last_button, disabled=disabled)

    disabled = self.play.value == 0
    self.set_button_status(self.prev_button, disabled=disabled)
    disabled = self.play.value == self.nodenum - 1
    self.set_button_status(self.next_button, disabled=disabled)
    
Mbtree_Anim.update_gui = update_gui 
```
</details>

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

```diff_python
def update_gui(self):
元と同じなので省略
+   if self.abfig is not None:
+       self.update_ab()
+       status = self.mbtree.ablist_by_score[self.play.value][3]
+       disabled = status == "start"
+       self.set_button_status(self.node_first_button, disabled=disabled)                           
+       self.set_button_status(self.node_prev_button, disabled=disabled)
+       disabled = status == "end"
+       self.set_button_status(self.node_next_button, disabled=disabled)                           
+       self.set_button_status(self.node_last_button, disabled=disabled)

    disabled = self.play.value == 0
    self.set_button_status(self.prev_button, disabled=disabled)
    disabled = self.play.value == self.nodenum - 1
    self.set_button_status(self.next_button, disabled=disabled)
    
Mbtree_Anim.update_gui = update_gui
```
</details>

上記の修正後に下記のプログラムを実行し、ルートノードの処理を開始したフレームと、評価値が確定したフレームを表示すると、実行結果のように __ボタンの色が正しく表示される__ ことが確認できます。

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4fd90c83-9bef-52c4-1527-edc7b2e093b9.png" width="300">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/cc7cca57-89f9-a2fc-73f8-92649a876e55.png" width="300">

# 今回の記事のまとめ

今回の記事では Mbtree_Anim に 4 つのボタンを追加することで、__選択中のノードに対するフレームの移動__ を行うことができるようにするという改良を行いました。


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

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

# 次回の記事

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

[^1]:`"start"` と `"end"` のフレームは、そのノードの端（edge）にあるフレームの状態（status）なので `edge_status` という名前にしました
[^2]:差分を表す difference の略です
[^3]:集合を表す [set](https://qiita.com/ysgeso/items/3b8967122379079a2a82#set-%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9) で表現してもかまいません