Pythonで〇×ゲームのAIを一から作成する　その124　リプレイ機能を利用した際のバグの検証と修正

# 目次と前回の記事

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

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

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

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

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

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

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

# `gui_play` の実行速度についての補足

前回の記事で説明し忘れていた点を最初に補足します。[前回の記事](https://qiita.com/ysgeso/items/f3fea1f8fd0eba561d06#marubatsu_gui-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE-__init__-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E4%BF%AE%E6%AD%A3)でMarubatsu_GUI クラスのインスタンスを作成する際に、ゲーム木のデータをファイルから読み込まなくなりました。そのため、`gui_play` メソッドを実行する際に待たされることが無くなっています。下記のプログラムを実行してそのことを確認して下さい。

```python
from util import gui_play

gui_play()
```

# リプレイ機能を利用した際のバグの検証と修正

前回の記事では、部分木を表示する際に利用する、局面と最善手・評価値の対応表を Dropdown で変更できるように Mbtree_GUI クラスを修正しましたが、その結果 __バグが発生__ しています。そのバグは、__リプレイ機能を利用した際に発生__ するバグで、例えば下記の手順で操作を行うと発生します。

* `gui_play()` を実行し、(0, 0)、(1, 0) の順で着手を行う
* __1 手前の局面を表示__ する __< ボタン[^1]をクリックする__ と、下図のように __ゲーム盤の表示__ と、GUI の部分木の赤枠の __選択されたノードの局面__ が __一致しなくなる__

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/94e975be-fa44-9d43-95a7-0251866b0860.png" width="300">

* さらに < ボタンをクリックすると下記のような __エラーが発生__ する

```
略
File c:\Users\ys\ai\marubatsu\124\tree.py:507, in Mbtree.create_subtree(self)
    505 self.selectednode = self.root
    506 for move in selectedmb.records[1:]:
--> 507     self.selectednode = self.selectednode.children_by_move[move]

KeyError: (1, 0)
```

上記の 2 種類のバグが発生する原因について少し考えてみて下さい。

## バグの原因の検証

実は、このバグを __修正するのは簡単__ ですが、このバグの __原因を理解して説明する__ のはそれほど __簡単ではない__ のでバグの原因の見つけ方を説明します。

バグは 2 つありますが、1 つ目のバグはエラーが発生しないバグなので、先にエラーが発生する 2 つ目のバグの原因について検証することにします。

### エラーメッセージの検証

上記のエラーメッセージから、エラーが Mbtree クラスの __`create_subtree` 内で発生__ したことがわかります。また、エラーの原因が __`self.selectednode.children_by_move`__ に代入された __dict に `(1, 0)` というキーが存在しない__ 可能性が高いことがわかります。

エラーメッセージの付近で行われる下記のプログラムの処理は、__`create_subtree`__ で作成した __部分木の中から__、__`selectedmb`__ を使って __選択されたノードを探す__ というものです。

```python
self.selectednode = self.root
for move in selectedmb.records[1:]:
    self.selectednode = self.selectednode.children_by_move[move]
```

上記のプログラムでは、`create_subtree` が作成した部分木の __ルートノードを表す `self.root`__ と、選択された局面の __棋譜を表す `selectedmb.records`__ 属性の値を利用して処理を行っているので、__それらの値がどのように計算されるかを検証__ することにします。

## < ボタンをクリックした時に行われる処理の検証

バグは __< ボタンをクリックした時に発生する__ ので、< ボタンをクリックした時に、上記の処理が行われるまでの処理を検証することにします。

### `on_prev_button_clicked` の処理

< ボタンをクリックすると、__Marubatsu_GUI__ クラスの __`create_event_handler`__ メソッド内で定義された __`on_prev_button_clicked`__ が呼び出され、以下の手順で処理が行われます。

* 9 行目の処理によって、実引数に __一手前の手数__ を記述して 3 行目で定義された `change_step` を呼び出す
* 4 行目で実引数に __一手前の手数__ を記述して、__Marubatsu__ クラスの __`change_step`__ メソッドを呼び出すことで、__`self.mb` を一手前の局面にする__
* 6 行目で GUI の部分木の描画を更新する

```python
1  def create_event_handler(self):
略
2     # step 手目の局面に移動する
3     def change_step(step):
4         self.mb.change_step(step)
5         # 描画を更新する
6         self.update_gui()  
7            
8     def on_prev_button_clicked(b=None):
9         change_step(self.mb.move_count - 1)
略
```

### Marubatsu クラスの `change_step` の処理

上記の 4 行目で呼び出される __Marubatsu__ クラスの __`change_step`__ は下記のプログラムのように定義されており、__`records`__ 属性に記録された __棋譜に従って__、__ゲーム開始時の局面から `step` 回の着手を行う__ ことで __`step` 手目の局面にする__ 処理を行います。

:::note
`change_step` について忘れた方は、[以前の記事](https://qiita.com/ysgeso/items/e857737e7f36a3a242bc#%E7%9D%80%E6%89%8B%E3%81%AE%E8%A8%98%E9%8C%B2%E3%81%AB%E3%82%88%E3%82%8B%E5%B1%80%E9%9D%A2%E3%81%AE%E5%86%8D%E7%8F%BE)を復習して下さい。
:::

```python
1  def change_step(self, step):
2      # step の範囲を正しい範囲に修正する
3      step = max(0, min(len(self.records) - 1, step))
4      records = self.records
5      self.restart()
6      for x, y in records[1:step+1]:
7          self.move(x, y)
8      self.records = records
```

__リプレイ機能__ では __`change_step`__ を使って __任意の手数の局面を計算__ していますが、その際に上記の棋譜のデータが記録された __`records` 属性を利用__ しています。そのため、`change_step` を実行した際に __`records` 属性の値が変化しないようにする__ 必要があり、その処理を上記の __4 行目と 8 行目__ で行っています。

:::note warn
__リプレイ機能__ で局面を移動しても、棋譜を表す __`records` 属性の値が変化しない__ ことを覚えておいてください。このことが、バグの原因になっているからです。
:::

### Marubatsu_GUI クラスの `update_gui` の処理

次に、__ゲーム盤の表示を更新__ するために __Marubatsu_GUI__ クラスの __`update_gui`__ メソッドが呼び出されます。`update_gui` メソッドでは __`self.mb`__ の __ゲーム盤の表示の更新__ を行いますが、先ほど行った処理によって __`self.mb` は一手前の局面__ になっているので __一手前の局面が表示__ されます。そのことは、< ボタンをクリックすることで実際に一手前の局面のゲーム盤が表示されることからも確認できます。

その後で、下記の 3 ~ 7 行目のプログラムで __GUI の部分木の表示の更新__ の処理を行います。、下記のプログラムの 5 行目で __一手前の局面を__ GUI の部分木の __選択されたノード__ とし、6 行目で __GUI の部分木の表示を更新__ する処理を行います。

```python
1  def update_gui(self):
略(ゲーム盤の表示の更新の処理)
2
3          if hasattr(self, "mbtree_gui"):
4              from tree import Node
5  
6              self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
7              self.mbtree_gui.update_gui()
```

### Mbtree_GUI クラスの `update_gui` の処理

上記の 7 行目で呼び出された __Mbtree_GUI__ クラスの __`update_gui`__ メソッドでは、下記の手順で __部分木の作成の処理__ が行われます。

* __2 ~ 7 行目__：計算する子孫ノードの深さの最大値を表す `maxdepth` を計算する
* __8 ~ 13 行目__：中心となるノードの局面を表す `centermb` を計算する
* __14、15 行目__：上記で計算したデータを使って、部分木を作成する処理を呼び出す 

```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          if self.selectednode.depth <= 6:
 9              centermb = self.selectednode.mb
10          else:
11              centermb = Marubatsu()
12              for x, y in self.selectednode.mb.records[1:7]:
13                  centermb.move(x, y)
14          self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
15                               "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
略
```

__`selectednode`__ に代入された __一手前の局面のノード__ は、(0, 0) の着手が行われた __1 手目の局面__ なので、上記の処理によって __`maxdepth` には `2`__ が、__`centermb` には選択されたノードと同じ局面__ が代入されて、部分木が作成されます。

下図は __< ボタンを一回クリック__ した際に表示される __GUI の部分木__ で、確かに __(0, 0) の着手が行われた局面を中心とする部分木が作成__ されていることが確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/59c14b36-8cd4-f91d-7ca1-9201d35229cc.png" width="300">

上記の表示で __間違っている__ のは __赤枠の選択されたノード__ なので、部分木を作成する処理の中で __選択されたノードを計算する処理を探す__ と、__`create_subtree`__ の下記のプログラムでその処理が行われていることがわかります。この処理は、__< を 2 回クリックした際にエラーが発生__ したプログラムなので、__1 つ目のバグの原因もこの処理である可能性が高い__ ことがわかりました。

```python
1  def create_subtree(略):
略
2      self.selectednode = self.root
3      for move in selectedmb.records[1:]:
4          self.selectednode = self.selectednode.children_by_move[move]
```

### `create_subtree` の処理

__バグの原因の可能性が高いプログラムを絞り込む__ ことができたので、上記のプログラムで行われる処理を検証することにします。

__`selectedmb`__ は、(0, 0)、(1, 0) の __2 回の着手を行った後__ で、Marubatsu クラスの `change_step` メソッドによって __(0, 0) の着手が行われた 1 手目の局面を計算__ したものです。先程説明したように、__`change_step` を実行しても__、その棋譜を表す __`records` 属性の値は変化しない__ ので、__`selectedmb.records`__ には __(0, 0) と (1, 0) の 2 手分の着手が記録__ されています。上記の 3 行目の for 文では、__`records`__ 属性に記録された __すべての着手に対する繰り返し処理__ が行われるので、__`selectednode`__ には、(0, 0) と (1, 0) の __2 手分の着手を行った局面に対するノードが計算__ されて代入されることになります。

これが < ボタンを 1 回クリックした際に、上部のゲーム盤には一手前の局面が表示されるが、下部の GUI の部分木の赤枠の __選択された局面が変化しない__ ことの原因です。

## バグの修正

従って、1 つ目のバグは、下記のプログラムの 7 行目のように、__棋譜に記録された着手__ を、現在の手数を表す __`move_count` 属性の数だけ行う__ ようにすることで修正することができます[^2]。また、実は __2 つ目のエラーが発生するバグの原因__ は、__1 つ目のバグと同じ__ なので、この修正によって __両方のバグを修正__ することができます。

```python
 1  from tree import Mbtree, Node
 2  from marubatsu import Marubatsu
 3  from copy import deepcopy
 4
 5  def create_subtree(self):   
元と同じなので省略
 6      self.selectednode = self.root
 7      for move in selectedmb.records[1:selectedmb.move_count+1]:
 8          self.selectednode = self.selectednode.children_by_move[move]
 9    
10  Mbtree.create_subtree = create_subtree
```

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

```python
from tree import Mbtree, Node
from marubatsu import Marubatsu
from copy import deepcopy

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:selectedmb.move_count+1]:
        self.selectednode = self.selectednode.children_by_move[move]
    
Mbtree.create_subtree = create_subtree
```
</details>

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

```diff_python
from tree import Mbtree, Node
from marubatsu import Marubatsu
from copy import deepcopy

def create_subtree(self):   
元と同じなので省略
    self.selectednode = self.root
-   for move in selectedmb.records[1:]:
+   for move in selectedmb.records[1:selectedmb.move_count+1]:
        self.selectednode = self.selectednode.children_by_move[move]
    
Mbtree.create_subtree = create_subtree
```
</details>

上記の修正後に下記のプログラムを実行し、先程と同じ手順で操作を行ってもバグが発生しなくなったことを確認して下さい。また、様々な着手とリプレイ機能のボタンの操作を行い、プログラムが正しく動作することを確認して下さい。

```python
gui_play()
```

## 2 つ目のバグでエラーが発生する原因

__1 つ目のバグ__ では __エラーが発生しない__ が、__2 つ目のバグ__ で __エラーが発生する__ 点が気になっている人がいるかもしれません。その理由について少し考えてみて下さい。

2 つ目のバグは、(0, 0)、(1, 0) の着手を行った後で、__< ボタンを 2 回クリック__ しているので、__ゲーム開始時__ の __深さ 0 のノード__ が GUI の部分木の __選択されたノード__ になります。

その場合は、Mbtree_GUI クラスの `update_gui` の下記の処理によって、__`maxdepth` には `1`__ が、__`centermb` にはゲーム開始時の局面__ が代入されて部分木が計算されます。

```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          if self.selectednode.depth <= 6:
 9              centermb = self.selectednode.mb
10          else:
11              centermb = Marubatsu()
12              for x, y in self.selectednode.mb.records[1:7]:
13                  centermb.move(x, y)
14          self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
15                               "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
略
```

その結果、作成される部分木は、下図のように __深さ 1 までの子孫ノードはすべて計算__ されますが、__深さ 2 以降の子孫ノード__ は __最善手を計算し続けた場合の局面のノードしか計算されません__。また、下図からわかるように __(0, 0) に着手を行ったノードの子ノード__ には、__(0, 0)、(1, 1) に着手__ を行ったノード __しか存在しません__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1ef5b951-8292-828e-9a8c-713b9ba0ddfc.png" width="300">

そのため、修正前の下記のプログラムを実行すると、ルートノードから (0, 0)、(1, 0) の順で着手を行ったノードを計算しようとした結果、__(0, 0) に着手を行ったノード__ の __`children_by_move` 属性に代入された dict__ には __(0, 1) というキーは存在しない__ ので、KeyError: (1, 0) というエラーが発生することになります。

```python
1  def create_subtree(略):
略
2      self.selectednode = self.root
3      for move in selectedmb.records[1:]:
4          self.selectednode = self.selectednode.children_by_move[move]
```

このことから、バグを修正する前に、__(0, 0)、(1, 1) の順で着手__ を行い、< ボタンを 2 回クリックした場合は、作成した部分木に (0, 0)、(1, 1) の着手を行ったノードが __偶然存在する__ ので __エラーは発生しない__ ことがわかります。興味がある方は、バグを修正する前のプログラムに戻して実際に試してみて下さい。

また、(0, 0)、(1, 1) の順で着手 を行った後で __< ボタンを 1 回クリック__ した際に作成される部分木は、下図のように __深さ 2 までの子孫ノードがすべて計算される__ ので、(0, 0) に着手を行ったノードの子ノードには (1, 0) に着手を行ったノードが存在します。そのため __2 手目までの着手__ を行った局面で __< ボタンを 1 回クリック__ した場合は、2 手目までにどのような着手を行っても __エラーは発生しません__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/453180cf-e84e-2995-9b30-e36548661be3.png" width="300">

このような、__バグがあるにも関わらす特定の条件ではエラーが発生しない__ バグは、< ボタンを 1 回だけクリックした場合のように、__偶然その条件を満たしてしまう__ とバグの存在が __みづけづらい__ ので __非常に厄介なバグ__ です。このようなバグは、今回の記事のように実際によく発生することがあるので、詳しく紹介しました。

# Marubatsu_GUI クラスの改良

__Marubatsu_GUI__ クラスの __改良__ をいくつか思いついたので実装することにします。

行う改良は以下の通りです。最初の 3 つは同様の処理を別の所で実装済なので、過去で行った実装を参考に実装することにします。

4 つ目以降の改良については次回の記事で行います。

* [FloatSlider でゲーム盤の表示の大きさを変更](https://qiita.com/ysgeso/items/71a02346a63d6de28db4)できるようにする
* GUI の部分木の表示の有無を切り替える「木」ボタンを、「評価値の表示」ボタンのように [ON/OFF の状態がわかるようにする](https://qiita.com/ysgeso/items/71a02346a63d6de28db4#%E8%A9%95%E4%BE%A1%E5%80%A4%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AE%E6%9C%89%E7%84%A1%E3%81%AB%E3%82%88%E3%82%8B%E3%83%9C%E3%82%BF%E3%83%B3%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AE%E8%89%B2%E3%81%AE%E5%A4%89%E6%9B%B4)
* GUI の部分木の選択されたノードを、ゲーム盤に表示されている局面のノードにリセットする「リ」ボタンを、GUI の部分木の表示が OFF になっている場合は[操作できないようにする](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)
* 現在の局面の状況がわかるようにする
* ゲーム盤のマスに、そのマスに着手を行った場合の局面の状況を表示する
* ゲーム盤のマスに、そのマスに着手を行った際の AI の評価値を表示できるようにする

最初の 3 つの改良はこれまでに同様の処理を行っているのでまとめて行うことにします。

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

`Mbtree_GUI` クラスでは、部分木に評価値を表示するかどうかを [`show_score`](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) という属性に代入しましたので、Marubatsu_GUI クラスでも __GUI の部分木を表示するか__ どうかを __`show_subtree`__ という属性に代入することにします。

まず、__最初に GUI の部分木を表示するか__ どうかを Marubatsu_GUI クラスの __`__init__`__ メソッドの仮引数 __`show_subtree`__ に代入することにします。

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

* __5 行目__：仮引数 `show_subtree` を追加する
* __8 行目__：`show_subtree` を同名の属性に代入する
* __10 行目__：`show_subtree` の値に応じて、部分木の [表示の有無を設定](https://qiita.com/ysgeso/items/b5e388878fef195d8f2c#ipywidgets-%E3%81%AE%E3%82%A6%E3%82%A3%E3%82%B8%E3%82%A7%E3%83%83%E3%83%88%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%82%92%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88%E3%82%8B%E6%96%B9%E6%B3%95)する

```python
 1  from marubatsu import Marubatsu_GUI
 2  from tkinter import Tk
 3  import os
 4
 5  def __init__(self, mb, params, names, ai_dict,
 6               scoretable_dict, show_subtree, seed, size):   
元と同じなので省略
 7      self.names = names
 8      self.show_subtree = show_subtree
元と同じなので省略
 9      self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
10      self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "None"
11   
12  Marubatsu_GUI.__init__ = __init__
```

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

```python
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os

def __init__(self, mb, params, names, ai_dict,
             scoretable_dict, show_subtree, seed, size):   
    if params is None:
        params = [{}, {}]
    if ai_dict is None:
        ai_dict = {}
    if names is None:
        names = [None, None]
    for i in range(2):
        if names[i] is None:
            if mb.ai[i] is None:
                names[i] = "人間"
            else:
                names[i] = mb.ai[i].__name__
    
    # JupyterLab からファイルダイアログを開く際に必要な前処理
    root = Tk()
    root.withdraw()
    root.call('wm', 'attributes', '.', '-topmost', True)  

    # save フォルダが存在しない場合は作成する
    if not os.path.exists("save"):
        os.mkdir("save")        
    
    self.mb = mb
    self.ai_dict = ai_dict
    self.params = params
    self.names = names
    self.show_subtree = show_subtree
    self.seed = seed
    self.size = size
    
    super(Marubatsu_GUI, self).__init__()
    
    from tree import Mbtree_GUI

    self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
    self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "None"    

Marubatsu_GUI.__init__ = __init__
```
</details>

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

```diff_python
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os

def __init__(self, mb, params, names, ai_dict,
-            scoretable_dict, seed, size):   
+            scoretable_dict, show_subtree, seed, size):   
元と同じなので省略
    self.names = names
+   self.show_subtree = show_subtree
元と同じなので省略
    self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
+   self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "None"
    
Marubatsu_GUI.__init__ = __init__
```
</details>

## Marubatsu クラスの `play` メソッドの修正

次に、Marubatsu_GUI クラスのインスタンスを作成する __Marubatsu__ クラスの __`play`__ メソッドを以下のプログラムのように修正します。

* __2 行目__：デフォルト値を `True` とする仮引数 `show_subtree` を追加する
* __5 行目__：実引数 `show_subtree=show_subtree` を追加する

```python
1  def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None,
2           show_subtree=True, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
3      if gui:
4          mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict, 
5                                 show_subtree=show_subtree, seed=seed, size=size)
元と同じなので省略
6
7  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None,
         show_subtree=True, verbose=True, seed=None, gui=False, size=3):
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
        
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
    self.verbose = verbose
    self.gui = gui
    
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict, 
                               show_subtree=show_subtree, seed=seed, size=size)
    else:
        mb_gui = None
        
    self.restart()
    return self.play_loop(mb_gui, params=params)

Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None,
-        verbose=True, seed=None, gui=False, size=3):
+        show_subtree=True, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    if gui:
        mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict,
-                              seed=seed, size=size)
+                              show_subtree=show_subtree, seed=seed, size=size)
元と同じなので省略

Marubatsu.play = play
```
</details>

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

__FloatSlider を作成する必要がある__ ので、下記のプログラムの 6、7 行目のように `create_widgets` メソッドを修正します。この処理は[以前の記事](https://qiita.com/ysgeso/items/71a02346a63d6de28db4#create_widgets-%E3%81%AE%E4%BF%AE%E6%AD%A3)と同様ですが、ゲーム盤の表示サイズを代入する `Marubatsu_GUI` クラスの __`__init__`__ メソッドの __仮引数 `size` のデフォルト値は `3`__ なので、`min` を `1.0`、`max` を `5.0`、`step` を `0.1` としました。

```python
1  import ipywidgets as widgets
2
3  def create_widgets(self):
元と同じなので省略
4      self.help_button = self.create_button("？", 34)
5        
6      self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
7                                             description="size", value=self.size)
元と同じなので省略
8    
9  Marubatsu_GUI.create_widgets = create_widgets
```

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

```python
import ipywidgets as widgets

def create_widgets(self):
    # 乱数の種の Checkbox と IntText を作成する
    self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
                                     indent=False, layout=widgets.Layout(width="100px"))
    self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
                                   layout=widgets.Layout(width="80px"))   

    # 読み書き、ヘルプのボタンを作成する
    self.load_button = self.create_button("開く", 50)
    self.save_button = self.create_button("保存", 50)
    self.show_tree_button = self.create_button("木", 34)
    self.reset_tree_button = self.create_button("リ", 34)
    self.help_button = self.create_button("？", 34)
        
    self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
                                           description="size", value=self.size)
    
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセット、待ったボタンを作成する
    self.change_button = self.create_button("変更", 50)
    self.reset_button = self.create_button("リセット", 80)
    self.undo_button = self.create_button("待った", 60)    
    
    # リプレイのボタンとスライダーを作成する
    self.first_button = self.create_button("<<", 50)
    self.prev_button = self.create_button("<", 50)
    self.next_button = self.create_button(">", 50)
    self.last_button = self.create_button(">>", 50)     
    self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()

    # print による文字列を表示する Output を作成する
    self.output = widgets.Output()       
    
    # ヘルプを表示する Output を作成し、表示の設定を行う
    self.help = widgets.Output()
    self.print_helpmessage()
    self.help.layout.display = "none"   
    
Marubatsu_GUI.create_widgets = create_widgets
```
</details>

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

```diff_python
import ipywidgets as widgets

def create_widgets(self):
元と同じなので省略
    self.help_button = self.create_button("？", 34)
        
+   self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
+                                          description="size", value=self.size)
元と同じなので省略
    
Marubatsu_GUI.create_widgets = create_widgets
```
</details>

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

上記で作成した FloatSlider は、下記のプログラムの 6 行目のように、新しく作成した HBox に配置することにします。なお、次回の記事で別のウィジェットを配置する予定なので、この HBox の内容が少ない点は気にする必要はありません。

* __6 行目__：FloatSlider を配置した HBox を作成する
* __8、10 行目__：`hbox` の名前の末尾の数字を一つずつずらす
* __12 行目__：6 行目で作成した HBox を表示するように修正する

```python
 1  def display_widgets(self):
 2      # 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
 3      hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button, 
 4                            self.show_tree_button, self.reset_tree_button, self.help_button])
 5      # ゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
 6      hbox2 = widgets.HBox([self.size_slider])
 7      # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
 8      hbox3 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
 9      # リプレイ機能のボタンを横に配置した HBox を作成する
10      hbox4 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
11      # hbox1 ~ hbox4、Figure、Output を縦に配置した VBox を作成し、表示する
12      display(widgets.VBox([hbox1, hbox2, hbox3, hbox4, self.fig.canvas, self.output, self.help])) 
13
14  Marubatsu_GUI.display_widgets = display_widgets
```

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

```python
def display_widgets(self):
    # 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button, 
                          self.show_tree_button, self.reset_tree_button, self.help_button])
    # ゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
    hbox2 = widgets.HBox([self.size_slider])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox3 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox4 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox4、Figure、Output を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3, hbox4, self.fig.canvas, self.output, self.help])) 

Marubatsu_GUI.display_widgets = display_widgets
```
</details>

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

```diff_python
def display_widgets(self):
    # 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button, 
                          self.show_tree_button, self.reset_tree_button, self.help_button])
    # ゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
+   hbox2 = widgets.HBox([self.size_slider])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
-   hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
+   hbox3 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
-   hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
+   hbox4 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox4、Figure、Output を縦に配置した VBox を作成し、表示する
-   display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output, self.help])) 
+   display(widgets.VBox([hbox1, hbox2, hbox3, hbox4, self.fig.canvas, self.output, self.help])) 

Marubatsu_GUI.display_widgets = display_widgets
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果のように FloatSlider が表示されるようになります。

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7ebde749-dc42-b847-2a9e-1cd5bf9e66f2.png" width="300">

## `create_event_handler` の修正

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

* __4 ~ 7 行目__：「木」ボタンをクリックした際に呼び出されるイベントハンドラを定義する。行う処理は Mbtree_GUI クラスの [`on_score_button_clicked`](https://qiita.com/ysgeso/items/71a02346a63d6de28db4#create_event_handler-%E3%81%AE%E4%BF%AE%E6%AD%A3-1) と同様だが、GUI の部分木の表示の有無の切り替えは `update_gui` では行えないので 6 行目で `show_subtree` 属性の値に応じて部分木の表示の有無の切り替える処理を行う点が異なる
* __11 ~ 15 行目__：FloatSlider の値を変更した際に呼び出されるイベントハンドラを定義する。行う処理は Mbtree_GUI クラスの [`on_size_slider_changed`](https://qiita.com/ysgeso/items/71a02346a63d6de28db4#create_event_handler-%E3%81%AE%E4%BF%AE%E6%AD%A3) と同様だが、`self.size` を幅と高さに設定している点が異なる
* __17 行目__：上記のイベントハンドラと FloatSlider を結び付ける

```python
 1  import math
 2
 3  def create_event_handler(self):
元と同じなので省略
 4      def on_show_tree_button_clicked(b=None):
 5          self.show_subtree = not self.show_subtree
 6          self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
 7          self.update_gui()
 8
元と同じなので省略 
 9      self.help_button.on_click(on_help_button_clicked)
10    
11      def on_size_slider_changed(changed):
12          self.size = changed["new"]
13          self.fig.set_figwidth(self.size)
14          self.fig.set_figheight(self.size)
15          self.update_gui()
16
17    self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
18    
19  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

```python
import math

def create_event_handler(self):
    # 乱数の種のチェックボックスのイベントハンドラを定義する
    def on_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b=None):
        path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                        initialdir="save")
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                self.params = data["params"] if "params" in data else [ {}, {} ]
                if "names" in data:
                    names = data["names"]
                else:
                    names = [ "人間" if mb.ai[i] is None else mb.ai[i].__name__ for i in range(2)]                       
                options = self.dropdown_list[0].options.copy()
                for i in range(2):
                    value = (self.mb.ai[i], self.params[i]) 
                    if not value in options.values():
                        options[names[i]] = value
                for i in range(2):
                    self.dropdown_list[i].options = options
                    self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])            
                change_step(data["move_count"])
                if data["seed"] is not None:                   
                    self.checkbox.value = True
                    self.inttext.value = data["seed"]
                else:
                    self.checkbox.value = False
                    
    def on_save_button_clicked(b=None):
        names = [ self.dropdown_list[i].label for i in range(2) ]     
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{names[0]} VS {names[1]} {timestr}"
        path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                            initialdir="save", initialfile=fname,
                                            defaultextension="mbsav")
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                    "params": self.params,
                    "names": names,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
                
    def on_show_tree_button_clicked(b=None):
        self.show_subtree = not self.show_subtree
        self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
        self.update_gui()
        
    def on_reset_tree_button_clicked(b=None):
        self.update_gui()
                
    def on_help_button_clicked(b=None):
        self.help.layout.display = "none" if self.help.layout.display is None else None
    
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    self.show_tree_button.on_click(on_show_tree_button_clicked)
    self.reset_tree_button.on_click(on_reset_tree_button_clicked)
    self.help_button.on_click(on_help_button_clicked)
    
    def on_size_slider_changed(changed):
        self.size = changed["new"]
        self.fig.set_figwidth(self.size)
        self.fig.set_figheight(self.size)
        self.update_gui()

    self.size_slider.observe(on_size_slider_changed, names="value")
    
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
        self.mb.play_loop(self, self.params)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        self.output.clear_output()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.update_gui()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    # step 手目の局面に移動する
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.update_gui()        

    def on_first_button_clicked(b=None):
        change_step(0)

    def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        if self.mb.move_count != changed["new"]:
            change_step(changed["new"])
        
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            with self.output:
                self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
                self.mb.play_loop(self, self.params)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
            "-": on_load_button_clicked,            
            "l": on_load_button_clicked,            
            "+": on_save_button_clicked,            
            "s": on_save_button_clicked,            
            "*": on_help_button_clicked,            
            "h": on_help_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                event.inaxes = True
                event.xdata = num % 3
                event.ydata = 2 - (num // 3)
                on_mouse_down(event)
            except:
                pass
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)   
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

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

```diff_python
import math

def create_event_handler(self):
元と同じなので省略
    def on_show_tree_button_clicked(b=None):
-       self.mbtree_gui.vbox.layout.display = "none" if self.mbtree_gui.vbox.layout.display is None else None
+       self.show_subtree = not self.show_subtree
+       self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
+       self.update_gui()
元と同じなので省略 
    self.help_button.on_click(on_help_button_clicked)
    
+   def on_size_slider_changed(changed):
+       self.size = changed["new"]
+       self.fig.set_figwidth(self.size)
+       self.fig.set_figheight(self.size)
+       self.update_gui()

+   self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

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

「木」ボタンと「リ」ボタンが、部分木の表示の有無によって表示が変わるように `update_widgets_status` メソッドを下記のプログラムのように修正します。

* __3 行目__：「木」ボタンの表示の色を、[部分木を表示するかどうかで変更](https://qiita.com/ysgeso/items/71a02346a63d6de28db4#gui-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE-set_button_color-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%AE%9A%E7%BE%A9)する
* __4 行目__：「リ」ボタンを、部分木を表示しない場合は操作できないようにする

```python
1  def update_widgets_status(self):
2  	  self.inttext.disabled = not self.checkbox.value
3     self.set_button_color(self.show_tree_button, self.show_subtree)    
4     self.set_button_status(self.reset_tree_button, not self.show_subtree)
元と同じなので省略
5
6  Marubatsu_GUI.update_widgets_status = update_widgets_status
```

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

```python
def update_widgets_status(self):
    self.inttext.disabled = not self.checkbox.value
    self.set_button_color(self.show_tree_button, self.show_subtree)    
    self.set_button_status(self.reset_tree_button, not self.show_subtree)    
    self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
    self.set_button_status(self.first_button, self.mb.move_count <= 0)
    self.set_button_status(self.prev_button, self.mb.move_count <= 0)
    self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
    self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)    
    # value 属性よりも先に max 属性に値を代入する必要がある点に注意！
    self.slider.max = len(self.mb.records) - 1
    self.slider.value = self.mb.move_count

Marubatsu_GUI.update_widgets_status = update_widgets_status
```
</details>

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

```diff_python
def update_widgets_status(self):
    self.inttext.disabled = not self.checkbox.value
+   self.set_button_color(self.show_tree_button, self.show_subtree)    
+   self.set_button_status(self.reset_tree_button, not self.show_subtree)    
元と同じなので省略
 
Marubatsu_GUI.update_widgets_status = update_widgets_status
```
</details>

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、以下の確認を行ってください。

* FloatSlider を操作することで __ゲーム盤の表示の大きさが変更される__
* 「木」ボタンをクリックして GUI の部分木の表示の有無を変更すると、__「木」ボタンの色が変わる__
* 「木」ボタンをクリックして GUI の部分木の表示を消すと、__「リ」ボタンが操作できなくなる__

```python
update_gui()
```

## 文字の表示の大きさの修正

上記の修正によって、ゲーム盤の表示の大きさを変更できるようになりましたが、ゲーム盤の大きさを変更しても表示される __[文字の大きさが変わらない](https://qiita.com/ysgeso/items/71a02346a63d6de28db4#%E8%A9%95%E4%BE%A1%E5%80%A4%E3%81%8C%E9%87%8D%E3%81%AA%E3%81%A3%E3%81%A6%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%82%8B%E5%95%8F%E9%A1%8C%E3%81%AE%E5%8E%9F%E5%9B%A0)__ という問題があります。そのため、例えばゲーム盤のサイズを 1.5 にすると、下図のように __文字がはみ出て表示される__ ようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/c84c5841-864e-d0cd-9c62-28788aef092e.png" width="200">

この問題を修正するためには、[以前の記事](https://qiita.com/ysgeso/items/71a02346a63d6de28db4#%E5%95%8F%E9%A1%8C%E3%81%AE%E4%BF%AE%E6%AD%A3%E6%96%B9%E6%B3%95)で説明したように、__文字の大きさ__ を __ゲーム盤の大きさ__ に __比例した大きさで表示する__ ように修正する必要があります。

ゲーム盤の文字の表示は __Marubatsu_GUI__ クラスの __`update_gui`__ メソッドで行われるので、文字を表示する処理を下記のプログラムのように修正します。

* __3、4 行目__：元のプログラムでは __`fontsize` は 20 に設定__ されており、__`self.size`__ には __初期設定では `3` が代入__ されるので、`fontsize` は __`20 / 3 * self.size`__ という式で計算することができるが、`20 / 3` は割り切れないので、__`7 * self.size`__ に修正した

```python
1  def update_gui(self):
元と同じなので省略
2      ax.text(1.5, 3.5, f"{self.dropdown_list[0].label}　VS　{self.dropdown_list[1].label}", 
3              fontsize=7*self.size, ha="center")   
元と同じなので省略
4      ax.text(0, -0.2, text, fontsize=7*self.size)
元と同じなので省略
5        
6  Marubatsu_GUI.update_gui = update_gui
```

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

```python
def update_gui(self):
    ax = self.ax
    ai = self.mb.ai

    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()

    # y 軸を反転させる
    ax.invert_yaxis()

    # 枠と目盛りを表示しないようにする
    ax.axis("off")   

    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label}　VS　{self.dropdown_list[1].label}", 
            fontsize=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(0, -0.2, text, fontsize=7*self.size)

    self.draw_board(ax, self.mb)

    self.update_widgets_status()

    if hasattr(self, "mbtree_gui"):
        from tree import Node

        self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
        self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

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

```diff_python
def update_gui(self):
元と同じなので省略
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label}　VS　{self.dropdown_list[1].label}", 
-           fontsize=20, ha="center")   
+           fontsize=7*self.size, ha="center")   
元と同じなので省略
-   ax.text(0, -0.2, text, fontsize=20)
+   ax.text(0, -0.2, text, fontsize=7*self.size)
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

上記の修正後に下記のプログラムを実行し、ゲーム盤の大きさを変更すると実行結果のように、文字の大きさも変更されることが確認できます。

```python
gui_play()
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/99257b3f-52ab-409c-b5ef-fdccbef3efa0.png" width="200">

## 枠の太さの表示の修正

ゲーム盤の大きさを小さくすると、上図のように __枠線が太くなるように見える__ という現象が発生します。これは、__matplotlib__ では、__線の太さ__ は文字の大きさと同様に、Figure の大きさや Axes の表示範囲を変えても __変わらないことが原因__ です。

ゲーム盤に表示する __線の太さ__ は __`draw_board`__ メソッドの __仮引数 `lw` に代入__ することで指定できるので、下記のプログラムのように修正することで、ゲーム盤の表示の大きさにあわせて線の太さが変わるようになります。

* __2 行目__：`draw_board` にキーワード引数 `lw=0.7*self.size` を記述する。仮引数 `lw` のデフォルト値が `2` なので、このような式にした

```python
1  def update_gui(self):
元と同じなので省略
2      self.draw_board(ax, self.mb, lw=0.7*self.size)
元と同じなので省略
3        
4  Marubatsu_GUI.update_gui = update_gui
```

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

```python
def update_gui(self):
    ax = self.ax
    ai = self.mb.ai

    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()

    # y 軸を反転させる
    ax.invert_yaxis()

    # 枠と目盛りを表示しないようにする
    ax.axis("off")   

    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label}　VS　{self.dropdown_list[1].label}", 
            fontsize=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(0, -0.2, text, fontsize=7*self.size)

    self.draw_board(ax, self.mb, lw=0.7*self.size)

    self.update_widgets_status()

    if hasattr(self, "mbtree_gui"):
        from tree import Node

        self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
        self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

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

```diff_python
def update_gui(self):
元と同じなので省略
    self.draw_board(ax, self.mb, lw=0.7*self.size)
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

上記の修正後に下記のプログラムを実行し、ゲーム盤の大きさを変更すると実行結果のように、文字の大きさも変更されることが確認できます。

```python
gui_play()
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0bc7e7e8-6617-3b5d-e663-3ed9182dca65.png" width="200">

# 今回の記事のまとめ

今回の記事では、リプレイ機能を利用した際のバグの検証と修正を行いました。また、Marubatsu_GUI クラスのいくつかの改良を行いました。

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

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

# 次回の記事

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

[^1]:ゲーム盤の上にあるリプレイ機能の < ボタンです。ゲーム盤の下にある GUI の部分木の ← ボタンではない点に注意して下さい
[^2]:`records` 属性に代入された list には、0 番ではなく 1 番の要素から着手のデータが記録されるので、`move_count` 手目までの着手を行うためには、`move_count+1` と記述する必要があります