Pythonで〇×ゲームのAIを一から作成する　その122　評価値が計算された部分木の動的な作成と Mbtree_GUI クラスによる表示

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/7689cffcd53b4319ae5f

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

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

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

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

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

# 評価値が計算された部分木の動的な作成と表示

今回の記事も前回に引き続き、__`draw_subtree`__ メソッドで表示する __部分木を動的に作成する処理__ の実装の続きを行います。

現状では、`create_subtree` で作成した部分木を `draw_subtree` で表示した際に、__評価値が表示されない__ という問題があります。`draw_subtree` で評価値を表示するためには、部分木の __それぞれのノードの `score` 属性に評価値を記録__ しておく必要がありますが、そのデータは `create_subtree` で部分木を作成する際に利用する、局面と最善手の対応表を表す __`bestmoves_by_board.dat` には保存されていません__。

そこで、局面と最善手の対応表のデータに、__局面と評価値の対応表のデータを加えた__ ものを作製し、それを使って `create_subtree` で __評価値を計算した部分木を作成__ するように修正することにします。そのようなデータを以降は「__局面と最善手・評価値の対応表のデータ__」と表記することにします。

## 局面と最善手・評価値の対応表のデータの作成

局面と評価値の対応表のデータは、util.py の中で定義した __`calc_and_save_bestmoves_by_board`__ という関数で作成し、ファイルに保存していました。局面と最善手・評価値の対応表のデータを作成してファイルに保存する処理は、__この関数と同様の処理__ で行うことができます。

### 局面と最善手・評価値の対応表のデータ構造

まず、局面と最善手・評価値の対応表のデータを __どのようなデータ構造で表現するか__ を決める必要があります。どのようなデータ構造にすれば良いかについて少し考えてみて下さい。

局面と最善手の対応表のデータは、下記のような __dict で表現__ しました。

* キーを局面を表す文字列とする
* キーの値をその局面の最善手の一覧を表す list で表現する

同様に、局面と最善手・評価値の対応表のデータは、下記のような dict で表現することができます。

* キーを局面を表す文字列とする
* キーの値をその局面の「最善手の一覧を表す list」と「評価値」を表すデータとする

「最善手の一覧表す list」と「評価値」をまとめて表現する方法としては、以下のような方法が考えられます。

* 0 番の要素を最善手の一覧表す list、1 番の要素を評価値とする __tuple（または list）で表現__ する
* `bestmoves` というキーの値に最善手の一覧を表す list を、`score` というキーの値に評価値を代入する __dict で表現__ する

どちらの方法を使っても構いませんが、前者はプログラムの記述が短くなる、後者はプログラムがわかりやすくなるという利点があります。本記事ではわかりやすさを重視して後者の方法を採用することにします。なお、上記以外の方法で表現しても構わないので、よりよい方法を思いついた方はその方法で実装してみて下さい。

### `calc_and_save_bestmoves_by_board` のメソッドとしての定義

局面と最善手・評価値の対応表のデータを作成してファイルに保存する処理を実装する前に、__局面と最善手の対応表のデータ__ を作成してファイルに保存する処理を行う、__`calc_and_save_bestmoves_by_board`__ に関する __修正を行う__ ことにします。

[以前の記事](https://qiita.com/ysgeso/items/0f83bcdf76bac5b0175f#ai_gt6-%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F-ai)ではこの処理を __通常の関数として定義__ しましたが、よく考えるとこの処理は __Mbtree クラスのインスタンスに対して行う処理__ なので、Mbtree クラスの __メソッドとして定義したほうが自然__ です。そこで、下記のプログラムのように、Mbtree クラスのメソッドとして `calc_and_save_bestmoves_by_board` を 定義する事にします。

* __6 行目__：仮引数 `mbtree` を `self` に修正する
* __8 行目__：`mbtree` を `self` に修正する

```python
 1  from tree import Mbtree
 2  import pickle
 3  import gzip
 4  from tqdm import tqdm
 5
 6  def calc_and_save_bestmoves_by_board(self, path):   
 7      bestmoves_by_board = {}
 8      for node in tqdm(self.nodelist):
 9          txt = node.mb.board_to_str()
10          if not txt in bestmoves_by_board.keys():
11              bestmoves_by_board[txt] = node.bestmoves
12
13      with gzip.open(path, "wb") as f:
14          pickle.dump(bestmoves_by_board, f)
15    
16      return bestmoves_by_board
17
18  Mbtree.calc_and_save_bestmoves_by_board = calc_and_save_bestmoves_by_board  
```
<details><summary><strong>行番号のないプログラム</strong></summary>

```python
from tree import Mbtree
import pickle
import gzip
from tqdm import tqdm

def calc_and_save_bestmoves_by_board(self, path):
    
    bestmoves_by_board = {}
    for node in tqdm(self.nodelist):
        txt = node.mb.board_to_str()
        if not txt in bestmoves_by_board.keys():
            bestmoves_by_board[txt] = node.bestmoves

    with gzip.open(path, "wb") as f:
        pickle.dump(bestmoves_by_board, f)
    
    return bestmoves_by_board

Mbtree.calc_and_save_bestmoves_by_board = calc_and_save_bestmoves_by_board  
```
</details>

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

```diff_python
from tree import Mbtree
import pickle
import gzip
from tqdm import tqdm

-def calc_and_save_bestmoves_by_board(mbtree, path):
+def calc_and_save_bestmoves_by_board(self, path):   
    bestmoves_by_board = {}
-   for node in tqdm(mbtree.nodelist):
+   for node in tqdm(self.nodelist):
        txt = node.mb.board_to_str()
        if not txt in bestmoves_by_board.keys():
            bestmoves_by_board[txt] = node.bestmoves

    with gzip.open(path, "wb") as f:
        pickle.dump(bestmoves_by_board, f)
    
    return bestmoves_by_board

Mbtree.calc_and_save_bestmoves_by_board = calc_and_save_bestmoves_by_board  
```
</details>

なお、util.py に記述していた `calc_and_save_bestmoves_by_board` はもう必要がなくなったので削除することにします。

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

次に __局面と最善手・評価値の対応表のデータ__ を作成してファイルに保存するメソッドを定義します。メソッドの名前は __`calc_and_save_bestmoves_and_score_by_board`__ とし、下記のプログラムのように定義します。

なお、メソッドの __名前が長すぎる__ と思った方は、簡略化した名前を付けてもかまいませんが、__簡略化しすぎるとプログラムがわかりづらくなってしまう__ 点に注意して下さい。本記事ではわかりやすさを重視して長い名前を採用することにします。

* __1 行目__：メソッドを定義する。仮引数は `calc_and_save_bestmoves_by_board` と同じ
* __2 行目__：作成したデータを記録する変数名を `bestmoves_and_score_by_board` とする
* __5、6、12 行目__：`bestmoves_by_board` を `bestmoves_and_score_by_board` に修正する
* __6 ~ 9 行目__：最善手の一覧と評価値のデータを先程説明した dict の形式で代入する

```python
 1  def calc_and_save_bestmoves_and_score_by_board(self, path):
 2      bestmoves_and_score_by_board = {}
 3      for node in tqdm(self.nodelist):
 4          txt = node.mb.board_to_str()
 5          if not txt in bestmoves_and_score_by_board.keys():
 6              bestmoves_and_score_by_board[txt] = {
 7                  "bestmoves": node.bestmoves,
 8                  "score": node.score,
 9              }
10
11      with gzip.open(path, "wb") as f:
12          pickle.dump(bestmoves_and_score_by_board, f)
13      
14    return bestmoves_and_score_by_board
15
16  Mbtree.calc_and_save_bestmoves_and_score_by_board = calc_and_save_bestmoves_and_score_by_board  
```

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

```python
def calc_and_save_bestmoves_and_score_by_board(self, path):
    bestmoves_and_score_by_board = {}
    for node in tqdm(self.nodelist):
        txt = node.mb.board_to_str()
        if not txt in bestmoves_and_score_by_board.keys():
            bestmoves_and_score_by_board[txt] = {
                "bestmoves": node.bestmoves,
                "score": node.score,
            }

    with gzip.open(path, "wb") as f:
        pickle.dump(bestmoves_and_score_by_board, f)
    
    return bestmoves_and_score_by_board

Mbtree.calc_and_save_bestmoves_and_score_by_board = calc_and_save_bestmoves_and_score_by_board  
```
</details>

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

```diff_python
-def calc_and_save_bestmoves_by_board(self, path):
+def calc_and_save_bestmoves_and_score_by_board(self, path):
-   bestmoves_by_board = {}
+   bestmoves_and_score_by_board = {}
    for node in tqdm(self.nodelist):
        txt = node.mb.board_to_str()
-       if not txt in bestmoves_by_board.keys():
+       if not txt in bestmoves_and_score_by_board.keys():
-           bestmoves_by_board[txt] = node.bestmoves
+           bestmoves_and_score_by_board[txt] = {
+               "bestmoves": node.bestmoves,
+               "score": node.score,
+           }

    with gzip.open(path, "wb") as f:
-       pickle.dump(bestmoves_by_board, f)
+       pickle.dump(bestmoves_and_score_by_board, f)
    
    return bestmoves_and_score_by_board

Mbtree.calc_and_save_bestmoves_and_score_by_board = calc_and_save_bestmoves_and_score_by_board  
```
</details>

上記を実行後に、下記のプログラムを実行して、下記の 2 種類のゲーム木のデータをファイルから読み込み、局面と最善手・評価値の対応表を作成してファイルに保存します。

* 通常の方法で評価値を計算した `aidata.mbtree`。bestmoves_and_score_by_board.dat という名前のファイルに保存する
* [最短の勝利を優先した評価値を計算](https://qiita.com/ysgeso/items/0f83bcdf76bac5b0175f#mbtree-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E4%BF%AE%E6%AD%A3)した `bftree_shortest_victory.mbtree`。bestmoves_and_score_by_board_shortest_victory.dat という名前のファイルに保存する

```python
mbtree = Mbtree.load("../data/aidata")
bestmoves_and_score_by_board = mbtree.calc_and_save_bestmoves_and_score_by_board(
       "../data/bestmoves_and_score_by_board.dat")
bftree_shortest_victory = Mbtree.load("../data/bftree_shortest_victory")
bestmoves_and_score_by_board_shortest_victory = \
    bftree_shortest_victory.calc_and_save_bestmoves_and_score_by_board(
       "../data/bestmoves_and_score_by_board_shortest_victory.dat")
```

実行結果

```
100%|██████████| 549946/549946 [00:00<00:00, 593251.04it/s]
100%|██████████| 549946/549946 [00:01<00:00, 456474.54it/s]
```

なお、作成されたファイルは、どちらも約 23 KB のように小さいので、ファイルから読み込む際に時間はほとんどかかりません。

ファイル名が長すぎると思った方は、もっと短い名前でデータ保存してもかまいませんが、メソッドの名前と同様に、簡略化しすぎると後からそのファイルを見た時にどのようなデータが記録されているかがわかりづらくなる点に注意して下さい。

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

下記のプログラムのように __Node__ クラスの __`__init__`__ メソッドを修正することで、__ノードの作成時__ に __評価値を計算して `score` 属性に代入__ することができます。なお、この修正は[前回の記事](https://qiita.com/ysgeso/items/7689cffcd53b4319ae5f#node-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE-__init__-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E4%BF%AE%E6%AD%A3)で ノードの作成時に最善手を計算して `bestmoves` 属性に代入するようにした際の修正とほぼ同じです。

* __3 行目__：仮引数 `bestmoves_by_board` を `bestmoves_and_score_by_board` に修正する
* __11 行目__：`bestmoves_by_board` を `bestmoves_and_score_by_board` に修正する
* __12 ~ 14 行目__：ゲーム盤を文字列に変換した値を利用して最善手の一覧と評価値が代入されたキーの値を取り出し、それぞれの値を `bestmoves` 属性と `score` 属性に代入する

```python
 1  from tree import Node
 2
 3  def __init__(self, mb, parent=None, depth=0, bestmoves_and_score_by_board=None):
 4      self.id = Node.count
 5      Node.count += 1
 6      self.mb = mb
 7      self.parent = parent
 8      self.depth = depth
 9      self.children = []
10      self.children_by_move = {}   
11      if bestmoves_and_score_by_board is not None:
12          bestmoves_and_score = bestmoves_and_score_by_board[self.mb.board_to_str()]
13          self.bestmoves = bestmoves_and_score["bestmoves"]
14          self.score = bestmoves_and_score["score"]
15        
16  Node.__init__ = __init__
```

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

```python
from tree import Node

def __init__(self, mb, parent=None, depth=0, bestmoves_and_score_by_board=None):
    self.id = Node.count
    Node.count += 1
    self.mb = mb
    self.parent = parent
    self.depth = depth
    self.children = []
    self.children_by_move = {}   
    if bestmoves_and_score_by_board is not None:
        bestmoves_and_score = bestmoves_and_score_by_board[self.mb.board_to_str()]
        self.bestmoves = bestmoves_and_score["bestmoves"]
        self.score = bestmoves_and_score["score"]
        
Node.__init__ = __init__
```
</details>

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

```diff_python
from tree import Node

-def __init__(self, mb, parent=None, depth=0, bestmoves_by_board=None):
+def __init__(self, mb, parent=None, depth=0, bestmoves_and_score_by_board=None):
    self.id = Node.count
    Node.count += 1
    self.mb = mb
    self.parent = parent
    self.depth = depth
    self.children = []
    self.children_by_move = {}   
-   if bestmoves_by_board is not None:
+   if bestmoves_and_score_by_board is not None:
-       self.bestmoves = bestmoves_by_board[self.mb.board_to_str()]
+       bestmoves_and_score = bestmoves_and_score_by_board[self.mb.board_to_str()]
+       self.bestmoves = bestmoves_and_score["bestmoves"]
+       self.score = bestmoves_and_score["score"]
        
Node.__init__ = __init__
```
</details>

## Node クラスの `calcchildren` メソッドの修正

[前回の記事](https://qiita.com/ysgeso/items/7689cffcd53b4319ae5f#node-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE-calc_children-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E4%BF%AE%E6%AD%A3)と同様に、Node クラスの `__init__` メソッドの修正にあわせて __Node クラスのインスタンスを作成する処理を修正__ する必要があります。まず、子ノードの一覧を作成する Node クラスの __`calc_children`__ メソッドを下記のプログラムのように修正します。

* __3 行目__：仮引数 `bestmoves_by_board` を `bestmoves_and_score_by_board` に修正する
* __9 行目__：`bestmoves_by_board` を `bestmoves_and_score_by_board` に修正する

```python
 1  from copy import deepcopy
 2
 3  def calc_children(self, bestmoves_and_score_by_board=None):
 4      self.children = []
 5      for x, y in self.mb.calc_legal_moves():
 6          childmb = deepcopy(self.mb)
 7          childmb.move(x, y)
 8          self.insert(Node(childmb, parent=self, depth=self.depth + 1,
 9                           bestmoves_and_score_by_board=bestmoves_and_score_by_board))
10
11  Node.calc_children = calc_children
```

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

```python
from copy import deepcopy

def calc_children(self, bestmoves_and_score_by_board=None):
    self.children = []
    for x, y in self.mb.calc_legal_moves():
        childmb = deepcopy(self.mb)
        childmb.move(x, y)
        self.insert(Node(childmb, parent=self, depth=self.depth + 1,
                         bestmoves_and_score_by_board=bestmoves_and_score_by_board))
        
Node.calc_children = calc_children
```
</details>

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

```diff_python
from copy import deepcopy

-def calc_children(self, bestmoves_by_board=None):
+def calc_children(self, bestmoves_and_score_by_board=None):
    self.children = []
    for x, y in self.mb.calc_legal_moves():
        childmb = deepcopy(self.mb)
        childmb.move(x, y)
        self.insert(Node(childmb, parent=self, depth=self.depth + 1,
-                        bestmoves_by_board=bestmoves_by_board))
+                        bestmoves_and_score_by_board=bestmoves_and_score_by_board))
        
Node.calc_children = calc_children
```
</details>

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

次に、Mbtree クラスの __`create_subtree`__ の中で __ノードを作成する処理を修正__ します。

__局面と最善手・評価値の対応表__ のデータは、`create_subtree` の仮引数 `subtree` に代入する dict の __`bestmoves_and_score_by_board`__ というキーの値に代入することにし、その値を使って下記のプログラムのようにノードを作成するように修正します。

* __4 行目__：`bestmoves_and_score_by_board` に局面と最善手・評価値の対応表のデータを代入するように修正する
* __5、7、8、9、11 行目__：`bestmoves_by_board` を `bestmoves_and_score_by_board` に修正する
* __9 行目__：元の `bestmoves_by_board[board_str]` には最善手の一覧を表す list が代入されていたが、修正後の `bestmoves_and_score_by_board[board_str]` には最善手の一覧と評価値を表す dict が代入されているので、__`bestmoves_and_score_by_board[board_str]["bestmoves"]`__ のように修正する必要がある点に注意すること

```python
 1  from marubatsu import Marubatsu
 2
 3  def create_subtree(self):
 4      bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
 5      self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略
 6                  childnode = Node(childmb, parent=node, depth=depth+1, 
 7                                   bestmoves_and_score_by_board=bestmoves_and_score_by_board)   
元と同じなので省略
 8                  node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略         
 9                      x, y = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
元と同じなので省略
10                      childnode = Node(childmb, parent=node, depth=depth+1, 
11                                       bestmoves_and_score_by_board=bestmoves_and_score_by_board)   
元と同じなので省略
12        
13  Mbtree.create_subtree = create_subtree
```

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

```python
from marubatsu import Marubatsu

def create_subtree(self):
    bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
    self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
    
    depth = 0
    nodelist = [self.root]
    centermb = self.subtree["centermb"]
    centerdepth = centermb.move_count
    records = centermb.records
    maxdepth = self.subtree["maxdepth"]
    while len(nodelist) > 0:
        childnodelist = []
        for node in nodelist:
            if depth < centerdepth - 1:
                childmb = deepcopy(node.mb)
                x, y = records[depth + 1]
                childmb.move(x, y)
                childnode = Node(childmb, parent=node, depth=depth+1, 
                                 bestmoves_and_score_by_board=bestmoves_and_score_by_board)   
                node.insert(childnode)
                childnodelist.append(childnode)
            elif depth < maxdepth:
                node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)                   
                if depth == centerdepth - 1:
                    for move, childnode in node.children_by_move.items():
                        if move == records[depth + 1]:
                            self.centernode = childnode
                            childnodelist.append(self.centernode)
                        else:
                            if childnode.mb.status == Marubatsu.PLAYING:
                                childnode.children.append(None)
                else:
                    childnodelist += node.children
            else:
                if node.mb.status == Marubatsu.PLAYING:
                    childmb = deepcopy(node.mb)
                    board_str = node.mb.board_to_str()               
                    x, y = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
                    childmb.move(x, y)
                    childnode = Node(childmb, parent=node, depth=depth+1, 
                                     bestmoves_and_score_by_board=bestmoves_and_score_by_board)   
                    node.insert(childnode)
                    childnodelist.append(childnode)
        nodelist = childnodelist
        depth += 1

    selectedmb = self.subtree["selectedmb"]
    self.selectednode = self.root
    for move in selectedmb.records[1:]:
        self.selectednode = self.selectednode.children_by_move[move]
        
Mbtree.create_subtree = create_subtree
```
</details>

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

```diff_python
from marubatsu import Marubatsu

def create_subtree(self):
-   bestmoves_by_board = self.subtree["bestmoves_by_board"]
+   bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
-   self.root = Node(Marubatsu(), bestmoves_by_board=bestmoves_by_board)
+   self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略
                childnode = Node(childmb, parent=node, depth=depth+1, 
-                                bestmoves_by_board=bestmoves_by_board)   
+                                bestmoves_and_score_by_board=bestmoves_and_score_by_board)   
元と同じなので省略
-               node.calc_children(bestmoves_by_board=bestmoves_by_board)
+               node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)     
元と同じなので省略         
-                   x, y = bestmoves_by_board[board_str][0]
+                   x, y = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
元と同じなので省略
                    childnode = Node(childmb, parent=node, depth=depth+1, 
-                                    bestmoves_by_board=bestmoves_by_board)   
+                                    bestmoves_and_score_by_board=bestmoves_and_score_by_board)   
元と同じなので省略
        
Mbtree.create_subtree = create_subtree
```
</details>

上記の修正後に、下記のプログラムを実行して[前回の記事](https://qiita.com/ysgeso/items/7689cffcd53b4319ae5f#%E9%81%B8%E6%8A%9E%E3%81%95%E3%82%8C%E3%81%9F%E3%83%8E%E3%83%BC%E3%83%89%E3%81%AE%E8%A8%88%E7%AE%97)と同様の下記のような部分木を作成して表示すると、実行結果のように評価値が表示されるようになったことが確認できます。ただし、__一番上の評価値の上部が灰色の背景の部分からはみ出して表示される__ という問題が生じます。この問題の原因と修正する方法について少し考えてみて下さい。

* 中心となるノードは (0, 0) と (1, 0) に着手を行った局面のノード
* 選択されたノードは、さらに (2, 0) に着手を行った局面のノード
* 中心となるノードから深さ 3 までの全ての子孫ノードを持つ部分木を計算し、それ以降の深さでは最善手を着手し続けた局面のノードを計算する

```python
mb = Marubatsu()
mb.move(0, 0)
mb.move(1, 0)
mb2 = Marubatsu()
mb2.move(0, 0)
mb2.move(1, 0)
mb2.move(2, 0)
maxdepth = 3
subtree = Mbtree(subtree={"centermb": mb, "selectedmb": mb2, "maxdepth": maxdepth, 
                          "bestmoves_and_score_by_board": bestmoves_and_score_by_board})
centernode = subtree.centernode
selectednode = subtree.selectednode
subtree.draw_subtree(centernode=centernode, selectednode=selectednode, maxdepth=maxdepth,
                     show_bestmove=True)
```

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

```diff_python
mb = Marubatsu()
mb.move(0, 0)
mb.move(1, 0)
mb2 = Marubatsu()
mb2.move(0, 0)
mb2.move(1, 0)
mb2.move(2, 0)
maxdepth = 3
subtree = Mbtree(subtree={"centermb": mb, "selectedmb": mb2, "maxdepth": maxdepth, 
-                         "bestmoves_by_board": bestmoves_by_board})
+                         "bestmoves_and_score_by_board": bestmoves_and_score_by_board})
centernode = subtree.centernode
selectednode = subtree.selectednode
subtree.draw_subtree(centernode=centernode, selectednode=selectednode, maxdepth=maxdepth,
                     show_bestmove=True)

```
</details>


実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/2077d508-68f7-23b5-6da8-8c5d5002c504.png" width="450">

## 問題の検証と修正

一番上の評価値が灰色の背景からはみ出るというバグは、[以前の記事](https://qiita.com/ysgeso/items/0f83bcdf76bac5b0175f#%E4%B8%8A%E9%83%A8%E3%81%AE%E8%A9%95%E4%BE%A1%E5%80%A4%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AE%E4%B8%80%E9%83%A8%E3%81%8C%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E5%95%8F%E9%A1%8C%E3%81%AE%E4%BF%AE%E6%AD%A3)で __Mbtree_GUI クラスを使って部分木を表示__ した際にも発生しました。その際には __Mbtree_GUI__ クラスで作成する、部分木を表示する __Figure の大きさ__ と __Axes の表示範囲を修正__ することでバグの修正を行いました。一方、先程のプログラムは Mbtree_GUI クラスからではなく、__直接 `draw_subtree` メソッドを呼び出して部分木を表示__ しているので、以前の記事の __Mbtree_GUI クラスの修正は反映されません__。

先程のプログラムのように、__キーワード引数 `ax` を記述せず__ に __`draw_subtree` で部分木を表示__ した場合は、__`draw_subtree`__ の中で Figure を作成し、Axes の表示範囲を設定しているので、その部分を一番上の評価値が灰色の背景からはみでないように修正する必要があります。下記は、そのように `draw_subtree` を修正したプログラムです。

* __7 行目__：一番上の評価値がはみでないようするために、Figrue の高さを 1 増やす
* __9 行目__：Axes の y 座標の表示範囲の最小値を 0 から -1 に修正する

```python
 1  import matplotlib.pyplot as plt
 2  import matplotlib.patches as patches
 3
 4  def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
 5                   isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 6      if ax is None:
 7          fig, ax = plt.subplots(figsize=(width * size, (height + 1) * size))
 8          ax.set_xlim(0, width)
 9          ax.set_ylim(-1, height)   
10          ax.invert_yaxis()
11          ax.axis("off")        
元と同じなので省略 
12            
13  Mbtree.draw_subtree = draw_subtree
```

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

```python
import matplotlib.pyplot as plt
import matplotlib.patches as patches

def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
                    isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
    
    def calc_darkness(node):
        """ノードを表示する暗さを計算して返す."""
        
        if show_bestmove:
            if node.parent is None:
                return 0
            elif node.mb.last_move in node.parent.bestmoves:
                return 0
            else:
                return 0.2
            
        if anim_frame is None:
            return 0
        index = node.score_index if isscore else node.id
        return 0.5 if index > anim_frame else 0
    
    self.nodes_by_rect = {}

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

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

```diff_python
import matplotlib.pyplot as plt
import matplotlib.patches as patches

def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
                    isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
    if ax is None:
-       fig, ax = plt.subplots(figsize=(width * size, height * size))
+       fig, ax = plt.subplots(figsize=(width * size, (height + 1) * size))
        ax.set_xlim(0, width)
-       ax.set_ylim(0, height)   
+       ax.set_ylim(-1, height)   
        ax.invert_yaxis()
        ax.axis("off")        
元と同じなので省略 
            
Mbtree.draw_subtree = draw_subtree
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果のように一番上の評価値が灰色の背景からはみでないようになります。

```python
subtree = Mbtree(subtree={"centermb": mb, "selectedmb": mb2, "maxdepth": maxdepth, 
                          "bestmoves_and_score_by_board": bestmoves_and_score_by_board})
centernode = subtree.centernode
selectednode = subtree.selectednode
subtree.draw_subtree(centernode=centernode, selectednode=selectednode, maxdepth=maxdepth,
                     show_bestmove=True)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3dd727ba-0cf4-2ad8-79df-1664c3ccfed8.png" width="450">

# Mbtree_GUI クラスの修正

次に __`create_subtree`__ によって __動的に作成した部分木を描画__ するように __Mbtree_GUI__ クラスを修正します。どのように修正すれば良いかについて少し考えてみて下さい。

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

下記は、現状の __Mbtree_GUI__ クラスの __`__init__`__ メソッドの定義です。

```python
1  def __init__(self, mbtree, show_score=True, size=0.15):
2      self.mbtree = mbtree
3      self.show_score = show_score
4      self.size = size
5      self.width = 50
6      self.height = 65
7      self.selectednode = self.mbtree.root
8      super(Mbtree_GUI, self).__init__()
```    

Mbtree_GUI クラスは、これまではゲーム木全体を表す Mbtree クラスのインスタンスを使って部分木の描画を行っていましたが、そのデータは __もう必要がない__ ので、そのデータを代入する __仮引数 `mbtree`__ を __`__init__` メソッドから削除する__ 必要があります。また、その代わりに __部分木動的に作成するために必要__ となる __局面と最善手・評価値の対応表__ のデータを代入する __仮引数を `__init__` メソッドに追加__ する必要があります。

また、上記の 7 行目では __選択されたノード__ を表す __`selectednode`__ 属性を、ゲーム木の __ルートノードのデータで初期化__ していますが、__仮引数 `mbtree` を削除__ してしまうと、この時点では __ゲーム木のデータは存在しない__ ので 7 行目を実行すると __エラーが発生__ してしまいます。7 行目で `selectednode` 属性に何を代入すればよいかについては、__現時点では判断できないので保留__ し、後で 7 行目の処理を修正することにします。

下記は `__init__` メソッドを修正したプログラムです。

* __3 行目__：仮引数 `mbtree` を削除し、代わりに部分木を動的な作成に必要となる、局面と最善手・評価値の対応表のデータを代入する仮引数 `bestmoves_and_score_by_board` を追加する
* __4 行目__：`mbtree` 属性に同名の仮引数を代入する処理を削除し、代わりに `bestmoves_and_score_by_board` 属性に同名の仮引数を代入する処理を追加する
* __9 行目__：このままでは `self.mbtree` に値が代入されていないのでエラーが発生するが、この部分の修正は後回しにする

```python
 1  from tree import Mbtree_GUI
 2
 3  def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):   
 4      self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
 5      self.show_score = show_score
 6      self.size = size
 7      self.width = 50
 8      self.height = 65
 9      self.selectednode = self.mbtree.root
10      super(Mbtree_GUI, self).__init__()
11    
12  Mbtree_GUI.__init__ = __init__
```

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

```python
from tree import Mbtree_GUI

def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):   
    self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
    self.show_score = show_score
    self.size = size
    self.width = 50
    self.height = 65
    self.selectednode = self.mbtree.root
    super(Mbtree_GUI, self).__init__()
    
Mbtree_GUI.__init__ = __init__
```
</details>

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

```diff_python
from tree import Mbtree_GUI

-def __init__(self, mbtree, show_score=True, size=0.15):   
+def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):   
-   self.mbtree = mbtree
+   self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
    self.show_score = show_score
    self.size = size
    self.width = 50
    self.height = 65
    self.selectednode = self.mbtree.root
    super(Mbtree_GUI, self).__init__()
    
Mbtree_GUI.__init__ = __init__
```
</details>

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

__部分木の表示__ の処理を行う `update_gui` メソッドは、これまでは下記のプログラムによって、下記の手順で部分木を表示していました。

* __2 ~ 7 行目__：選択されたノードを表す __`self.selectednode`__ の深さ（`depth` 属性）から、__`maxdepth` を計算__ する
* __8 ~ 10 行目__：`self.selectednode` から __中心となるノード__ を下記の手順で __計算する__
    * 深さが 6 以下の場合は、選択されたノードを中心となるノードとする
    * 深さが 6 より大きい場合は、__親ノード（`parent` 属性）をさかのぼって__、深さが 6 の先祖ノードを中心となるノードとする
* __11 ~ 13 行目__：ゲーム木全体のデータが代入された `self.mbtree` の `draw_subtree` メソッドを呼び出して部分木を描画する

```python
 1  def update_gui(略):
略
 2          if self.selectednode.depth <= 4:
 3              maxdepth = self.selectednode.depth + 1
 4          elif self.selectednode.depth == 5:
 5              maxdepth = 7
 6          else:
 7              maxdepth = 9
 8          centernode = self.selectednode
 9          while centernode.depth > 6:
10              centernode = centernode.parent
11          self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
12                                   show_bestmove=True, show_score=self.show_score,
13                                   ax=self.ax, maxdepth=maxdepth, size=self.size)
略
```                                

上記の処理を、__部分木を動的に作成__ し、__その部分木を表示する__ ように修正する必要があります。部分木を動的に作成する __`create_subtree`__ メソッドは、__選択されたノードの局面__ を表すデータ、__中心となるノードの局面__ を表すデータ、すべての子孫ノードを計算するノードの __最大の深さを表す `maxdepth`__ を必要とし、それらのデータは上記のプログラムの 2 ~ 10 行目の処理を行った結果、__下記の変数に代入__ されます。

| データ | 代入される変数 |
|:--|:--|
| __選択されたノードの局面を表すデータ__  | `self.selectednode.mb`  |
| __中心となるノードの局面を表すデータ__  | `centernode.mb`  |
| __最大の深さ__  | `maxdepth`  |

従って、__上記のデータを利用__ する事で __部分木を動的に作成__ して __描画__ を行えます。

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

* __2 ~ 10 行目__：この部分のプログラムは修正する必要はない
* __11、12 行目__：2 ~ 10 行目で計算したデータを利用して部分木を作成し、Mbtree_GUI クラスが表示するゲーム木のデータを表す __`mbtree` 属性に代入__ する
* __13 行目__：__`self.mbtree`__ に代入した部分木は、__新しく作成__ したものなので、その中の __選択されたノードを表す `self.mbtree.selectednode`__ は、__`self.selectednode`__ に代入されているノード __とは別のノード__ である。そのため、__`self.selectednode`__ に新しく作成された部分木の選択されたノードを代入して __更新する必要__ がある
* __14 行目__：同様に、中心となるノードも `centernode` に代入されているノードとは別のノードなので __`centernode`__ を __新しく作成した部分木の中心となるノードに更新__ する

```python
 1  def update_gui(self):
元と同じなので省略
 2      if self.selectednode.depth <= 4:
 3          maxdepth = self.selectednode.depth + 1
 4      elif self.selectednode.depth == 5:
 5          maxdepth = 7
 6      else:
 7          maxdepth = 9
 8      centernode = self.selectednode
 9      while centernode.depth > 6:
10          centernode = centernode.parent
11      self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
12                           "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
13      self.selectednode = self.mbtree.selectednode
14      centernode = self.mbtree.centernode
15      self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
16                               show_bestmove=True, show_score=self.show_score,
17                               ax=self.ax, maxdepth=maxdepth, size=self.size)
元と同じなので省略
18    
19  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(-1, self.height - 1)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    if self.selectednode.depth <= 4:
        maxdepth = self.selectednode.depth + 1
    elif self.selectednode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    centernode = self.selectednode
    while centernode.depth > 6:
        centernode = centernode.parent
    self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
                         "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
    self.selectednode = self.mbtree.selectednode
    centernode = self.mbtree.centernode
    self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
                             show_bestmove=True, show_score=self.show_score,
                             ax=self.ax, maxdepth=maxdepth, size=self.size)
    
    disabled = self.selectednode.parent is None
    self.set_button_status(self.left_button, disabled=disabled)
    disabled = self.selectednode.depth >= 6 or len(self.selectednode.children) == 0
    self.set_button_status(self.right_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children.index(self.selectednode) == 0
    self.set_button_status(self.up_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children[-1] is self.selectednode
    self.set_button_status(self.down_button, disabled=disabled)
    self.set_button_color(self.score_button, value=self.show_score)
    
Mbtree_GUI.update_gui = update_gui
```
</details>

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

```diff_python
def update_gui(self):
元と同じなので省略
    if self.selectednode.depth <= 4:
        maxdepth = self.selectednode.depth + 1
    elif self.selectednode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    centernode = self.selectednode
    while centernode.depth > 6:
        centernode = centernode.parent
+   self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
+                        "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
+   self.selectednode = self.mbtree.selectednode
+   centernode = self.mbtree.centernode
    self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
                           show_bestmove=True, show_score=self.show_score,
                           ax=self.ax, maxdepth=maxdepth, size=self.size)
元と同じなので省略
    
Mbtree_GUI.update_gui = update_gui
```
</details>

上記の 2 ~ 14 行目で行った修正によって、__`self.mbtree`__、__`self.selectednode`__、__`centernode`__ に __新しく作成した部分木に対応するデータが代入__ されるため、それらの値を使って __部分木の表示を行う 15 行目__ のプログラムを __変更する必要はありません__。

また、2 ~ 14 行目では __それら以外の変数や属性の値を変更する処理は行っていない__ ので、Mbtree_GUI クラスの __他のプログラムを修正する必要はありません__。例えば、`create_event_handler` メソッドに記述されている処理を変更する必要はありません。

## `__init__` メソッド内で `selectenode` 属性に代入する値

上記で他のプログラムを修正する必要はないと説明しましたが、それは __`update_gui`__ の 2 ~ 14 行目の処理が __行われた後__ で実行される処理を修正する必要はないという意味です。

先程保留した __`__init__`__ メソッドで __`selectednode` 属性の初期化__ を行う処理は、__`update_gui`__ の __2 ~ 14 行目の処理を行う前に実行される__ ため、`update_gui` の 2 ~ 14 行目の処理を __正しく行うことができるようなデータで初期化する__ 必要があります。

`update_gui` の 2 ~ 14 行目で、__`self.selectednode` に対して行われる処理__ は、`selectednode` の深さを表す __`depth` 属性__ と、親ノードを表す __`parent` 属性__ に対する処理だけです。従って、__それらの属性に適切なデータが代入__ されている __ルートノードのデータ__ で `self.selectednode` を __初期化すればよい__ ことがわかります。

従って、`__init__` メソッドの中で __`self.selectednode`__ に代入する __ルートノードを表すデータ__ は、下記の理由から __`Node(Marubatsu())`__ を代入すればよいことがわかります。

* ゲーム開始時の局面のデータは `Marubatsu()` によって作成できる
* ルートノードには親ノードは存在しないので、キーワード引数 `parent` を記述する必要はない
* ルートノードの深さは 0 で、Node クラスの `__init__` メソッドの深さを代入する仮引数 `depth` はデフォルト値が 0 として定義されているので、キーワード引数 `depth` を記述する必要はない
* 最善手の一覧や評価値のデータを計算する必要はないので、キーワード引数 `bestmoves_and_score_by_board` を記述する必要はない
* 子ノードのデータは必要がないので、ルートノードを作成した後で、__子ノードのデータを計算する必要はない__

下記はそのように `__init__` メソッドを修正したプログラムです。

* __8 行目__：__ゲーム開始時の局面を表すノードを作成__ して __`selectednode` 属性に代入__ する

```python
1  def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):   
2      self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
3      self.show_score = show_score
4      self.size = size
5      self.width = 50
6      self.height = 65
7      self.selectednode = Node(Marubatsu())
8      super(Mbtree_GUI, self).__init__()    
9  Mbtree_GUI.__init__ = __init__
```

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

```python
def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):   
    self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
    self.show_score = show_score
    self.size = size
    self.width = 50
    self.height = 65
    self.selectednode = Node(Marubatsu())
    super(Mbtree_GUI, self).__init__()
    
Mbtree_GUI.__init__ = __init__
```
</details>

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

```diff_python
def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):   
    self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
    self.show_score = show_score
    self.size = size
    self.width = 50
    self.height = 65
-   self.selectednode = self.mbtree.root
+   self.selectednode = Node(Marubatsu())
    super(Mbtree_GUI, self).__init__()    
Mbtree_GUI.__init__ = __init__
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果のようにエラーが発生します。このエラーの原因について少し考えてみて下さい。

```python
Mbtree_GUI(bestmoves_and_score_by_board)
```

実行結果

```
略
Cell In[11], line 20
     17 self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
     18                      "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
     19 self.selectednode = self.mbtree.selectednode
---> 20 centernode = self.mbtree.centernode
     21 self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
     22                          show_bestmove=True, show_score=self.show_score,
     23                          ax=self.ax, maxdepth=maxdepth, size=self.size)
     25 disabled = self.selectednode.parent is None

AttributeError: 'Mbtree' object has no attribute 'centernode'
```

## エラーの原因の検証と修正

エラーメッセージから __`centernode=self.mbtree.centernode`__ を実行した際に、__`self.mbtree` に `centernode` 属性が存在しない__ ことがわかります。そこで、__`create_subtree`__ 内で __`centernode` 属性を計算する処理を検証__ ことにします。

下記は `create_subtree` の `centernode` に値を代入する処理に関連するプログラムです。

```python
 1  def create_subtree(self):
 2      bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
 3      self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
 4   
 5      depth = 0
 6      nodelist = [self.root]
略
 7      while len(nodelist) > 0:
略
 8                  node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)                   
 9                  if depth == centerdepth - 1:
10                      for move, childnode in node.children_by_move.items():
11                          if move == records[depth + 1]:
12                              self.centernode = childnode
略                            
```

上記から、`centernode` に値を代入する処理は、7 行目の `while` 文による繰り返しの処理の中で、以下の手順で行われることがわかります。

* 8 行目で `node` の子ノードを作成する
* 11 行目で `node` の子ノードが中心となるノードと判定された場合に、12 行目で `centernode` 属性にその子ノードを代入する

一方、部分木の __ルートノードは 3 行目で作成__ されますが、__ルートノード__ は他のノードの __子ノードではありません__。そのため、__ルートノードが中心となるノードの場合__ は、11 行目の処理が実行されることはないため、__`centernode` 属性に値は代入されません__。

Mbtree_GUI クラスの __`__init__`__ メソッドでは、__ルートノードを `selectednode` に代入__ し、その場合は `update_gui` 内の処理で __ルートノードが中心となるノードとなる__ ので __`centernode` 属性は計算されず__、先程のようなエラーが発生することになります。

従って、このエラーは下記のプログラムのように、__中心となるノードがルートノードの場合__ に __`centernode` 属性にルートノードを代入する__ ことで修正することができます。

* __9、10 行目__：深さが 0 のノードはルートノードのみなので、中心となるノードの深さが 0 の場合に `centernode` 属性にルートノードを代入する 

```python
 1  def create_subtree(self):
元と同じなので省略
 2      bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
 3      self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
 4   
 5      depth = 0
 6      nodelist = [self.root]
 7      centermb = self.subtree["centermb"]
 8      centerdepth = centermb.move_count
 9      if centerdepth == 0:
10          self.centernode = self.root
元と同じなので省略
11        
12  Mbtree.create_subtree = create_subtree
```

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

```python
def create_subtree(self):
    bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
    self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
    
    depth = 0
    nodelist = [self.root]
    centermb = self.subtree["centermb"]
    centerdepth = centermb.move_count
    if centerdepth == 0:
        self.centernode = self.root
    records = centermb.records
    maxdepth = self.subtree["maxdepth"]
    while len(nodelist) > 0:
        childnodelist = []
        for node in nodelist:
            if depth < centerdepth - 1:
                childmb = deepcopy(node.mb)
                x, y = records[depth + 1]
                childmb.move(x, y)
                childnode = Node(childmb, parent=node, depth=depth+1, 
                                 bestmoves_and_score_by_board=bestmoves_and_score_by_board)   
                node.insert(childnode)
                childnodelist.append(childnode)
            elif depth < maxdepth:
                node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)                   
                if depth == centerdepth - 1:
                    for move, childnode in node.children_by_move.items():
                        if move == records[depth + 1]:
                            self.centernode = childnode
                            childnodelist.append(self.centernode)
                        else:
                            if childnode.mb.status == Marubatsu.PLAYING:
                                childnode.children.append(None)
                else:
                    childnodelist += node.children
            else:
                if node.mb.status == Marubatsu.PLAYING:
                    childmb = deepcopy(node.mb)
                    board_str = node.mb.board_to_str()               
                    x, y = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
                    childmb.move(x, y)
                    childnode = Node(childmb, parent=node, depth=depth+1, 
                                     bestmoves_and_score_by_board=bestmoves_and_score_by_board)   
                    node.insert(childnode)
                    childnodelist.append(childnode)
        nodelist = childnodelist
        depth += 1

    selectedmb = self.subtree["selectedmb"]
    self.selectednode = self.root
    for move in selectedmb.records[1:]:
        self.selectednode = self.selectednode.children_by_move[move]
        
Mbtree.create_subtree = create_subtree
```
</details>

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

```diff_python
def create_subtree(self):
元と同じなので省略
    bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
    self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
    
    depth = 0
    nodelist = [self.root]
    centermb = self.subtree["centermb"]
    centerdepth = centermb.move_count
    if centerdepth == 0:
        self.centernode = self.root
元と同じなので省略
        
Mbtree.create_subtree = create_subtree
```
</details>

実行結果は省略しますが、上記の修正後に下記のプログラムを実行するとルートノードが選択された部分木が表示され、上部のボタンをクリックしたり、ノードをクリックするとこれまでと同様に選択されたノードが正しく変更されます。実際に確認してみて下さい。

```python
Mbtree_GUI(bestmoves_and_score_by_board)
```



また、下記のプログラムのように、__最短の勝利を優先した評価値を計算したデータ__ で Mbtree_GUI クラスのインスタンスを作成すると、実行結果のように部分木にそのような評価値が表示されるようになることが確認できます。なお、下図では 2.0 という評価値が表示されるように、(1, 0) に着手を行った局面を選択状態にしました。

```python
Mbtree_GUI(bestmoves_and_score_by_board_shortest_victory)
```
実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b63f1583-6e02-b3a4-2f3e-51a74f7bfb33.png" width="450">

# 本記事のまとめ

本記事では、局面と最善手・評価値の対応表のデータを作成してファイルに保存するプログラムを作成し、そのデータ利用して部分木を表示するように Mbtree_GUI を修正しました。また、それによって異なる方法で評価値を計算したゲーム木の部分木を表示することができることを示しました。

次回の記事では Mbtree_GUI クラスで表示するゲーム木の種類を後から変更できるようにし、`gui_play` で部分木を正しく表示できるように修正する予定です。

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

| リンク | 説明 |
|:--|:--|
| [marubatsu.ipynb](https://github.com/ysgeso/marubatsu/blob/master/122/marubatsu.ipynb)  | 本記事で入力して実行した JupyterLab のファイル |
| [tree_new.py](https://github.com/ysgeso/marubatsu/blob/master/122/tree_new.py)  | 今回の記事で更新した tree.py |
| [util_new.py](https://github.com/ysgeso/marubatsu/blob/master/122/tree_new.py)  | 今回の記事で更新した util_new.py |
| [bestmoves_and_score_by_board.dat](https://github.com/ysgeso/marubatsu/blob/master/data/bestmoves_and_score_by_board.dat) | 今回の記事で作成した、局面と最善手・評価値の対応表のデータを保存したファイル |
| [bestmoves_and_score_by_board_shortest_victory.dat](https://github.com/ysgeso/marubatsu/blob/master/data/bestmoves_and_score_by_board_shortest_victory.dat) | 今回の記事で作成した、最短の勝利を優先した評価値を計算した局面と最善手・評価値の対応表のデータを保存したファイル |

# 次回の記事

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