Pythonで〇×ゲームのAIを一から作成する　その114　データ構造を変更した結果生じた Marubatsu_GUI クラスのバグの修正

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/903dc60e865ff56191fc

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

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

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

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

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

# Marubatsu_GUI クラスのバグの修正と改良

前回の記事で Marubatsu_GUI に対して修正を行った結果、いくつかのバグが発生しているので、今回の記事ではそれらのバグの修正といくつかの改良を行うことにします。

今回の記事で紹介するようなバグは、プログラムの __データ構造などを後から変更する__ 場合などで良く発生するバグです。特に、本記事で行っているように、__[その場の思い付きでプログラムを改良していく](https://qiita.com/ysgeso/items/1b7b1abe942e2d10c0b4#%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E6%A9%9F%E8%83%BD%E3%81%AE%E4%BB%95%E6%A7%98)__ という、__日曜大工的なプログラミングにはつきもののバグ__ でしょう。

## `gui_play` にキーワード引数 `ai` を記述した場合のバグ

最初のバグは、普通に `gui_play()` を呼び出しているだけでは気づくこと難しいバグだと思います。筆者も最初はこのバグに気づかずに、様々な条件で `gui_play` を呼び出した際にこのバグ発見しました。

### バグが発生する状況

具体的には、`gui_play` にキーワード引数 `ai` を記述して、__最初に特定の AI どうしの対戦を行う__ ようにし、その後で __リセットボタンをクリックする__ とエラーが発生します。

例えば、下記のプログラムで `gui_play` で最初に `ai2s` VS `ai2s` どうしの対戦を行うと、実行結果のように正しく対戦が行われます。なお、対戦する AI はどの AI でもかまいません。

```python
from util import gui_play
from ai import ai2s

gui_play(ai=[ai2s, ai2s])
```

実行結果（実行結果はランダムなので下記と異なる場合があります）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/ba555231-e4b7-5d43-e3b6-0418d822895b.png" width="200">

しかし、その後でリセットボタンをクリックしてもう一度 `ai2s` VS `ai2s` どうしの対戦を行おうとすると、下記のようなエラーが発生します。

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File c:\Users\ys\ai\marubatsu\114\marubatsu.py:776, in Marubatsu_GUI.create_event_handler.<locals>.on_reset_button_clicked(b)
    774 self.mb.restart()
    775 self.output.clear_output()
--> 776 on_change_button_clicked(b)

File c:\Users\ys\ai\marubatsu\114\marubatsu.py:765, in Marubatsu_GUI.create_event_handler.<locals>.on_change_button_clicked(b)
    763 def on_change_button_clicked(b):
    764     for i in range(2):
--> 765         self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
    766                                                 else self.dropdown_list[i].value
    767     self.mb.play_loop(self, self.params)

TypeError: cannot unpack non-iterable function object
```

__1 回目__ の `ai2s` VS `ai2s` は __正しく動作する__ が、リセットボタンを押した後の __2 回目の対戦ではエラーが発生する__ 点が不思議だと思っている人が多いかもしれません。このエラーの原因について少し考えてみて下さい。

### 1 回目の `ai2s` VS `ai2s` でエラーが発生しない理由の検証

上記のエラーメッセージから、下記のプログラムを実行した際に「反復可能オブジェクトでない（non-iterable）関数オブジェクト（function object）を展開（unpack）することはできない」というメッセージが表示されたことがわかります。

```python
self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                           else self.dropdown_list[i].value
```

`ai2s` VS `ai2s` は AI どうしの対戦なので、`self.dropdown_list[i].value == "人間"` が `True` になることはないはずです。そのため、上記のプログラムでは下記の処理が行われた可能性が高く、Dropdown の __選択中の項目の値__ を表す __`self.dropdown_list[i].value`__ に __関数が代入されている可能性が高い__ ことがわかります。

```python
self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
```

また、下記のエラーメッセージから、エラーが発生するまでの間に下記の処理が行われたことが確認できます。

* `create_event_handler` 内で定義された、リセットボタンがクリックされた際に呼び出される __`on_reset_button_clicked`__ が呼び出された
* その中で `on_change_button_clicked(b)` が呼び出された
* `on_chage_button_clicked` の処理の中でエラーが発生した

```
File c:\Users\ys\ai\marubatsu\114\marubatsu.py:776, in 
     Marubatsu_GUI.create_event_handler.<locals>.on_reset_button_clicked(b)
    774 self.mb.restart()
    775 self.output.clear_output()
--> 776 on_change_button_clicked(b)
```


1 回目の `ai2s` VS `ai2s` の対戦の処理は、`gui_play` を実行した後ですぐに完了し、その際にリセットボタンや変更ボタンのクリックは行われないので、__先程のエラーの原因となった `on_reset_button_clicked` も `on_reset_button_clicked` も実行されません__。これが、1 回目 の `ai2s` VS `ai2s` でエラーが発生しない理由です。

### 2 回目の `ai2s` VS `ai2s` でエラーが発生する理由の検証

先程示したように、 2 回目の `ai2s` VS `ai2s` では、下記の処理によってエラーが発生した可能性が高いです。また、その際に Dropdown を変更する操作は行っていないので、選択中の Dropdown の項目の値を表す __`self.dropdown_list[i].value`__ に代入された値は、__Dropdown を作成してから変更されていない可能性が高い__ ことがわかります。

```python
self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
```

そこで、下記のプログラムのように、Dropdown を作成する `create_dropdown` 内で、作成した Dropdown の `value` 属性の値を `print` で表示してみることにします。

* __14 行目__：11 行目から、作成する Dropdown の __`value` 属性 には `select_values[i]` を代入している__ ことがわかるので、__その値を `print` で表示する__ ようにする

```python
 1  from marubatsu import Marubatsu_GUI
 2  import ipywidgets as widgets
 3
 4  def create_dropdown(self):
元と同じなので省略
 5          self.dropdown_list.append(
 6              widgets.Dropdown(
 7                  options=self.ai_dict,
 8                  description=description,
 9                  layout=widgets.Layout(width="100px"),
10                  style={"description_width": "20px"},
11                  value=select_values[i],
12              )
13          )
14          print(select_values[i])   
15
16  Marubatsu_GUI.create_dropdown = create_dropdown
```

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

```python
from marubatsu import Marubatsu_GUI
import ipywidgets as widgets

def create_dropdown(self):
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 〇 と × の Dropdown を格納する list
    self.dropdown_list = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = self.mb.ai[i].__name__        
            value = self.mb.ai[i]
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value
    
        # Dropdown の description を計算する
        description = "〇" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )
        print(select_values[i])   

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

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

```diff_python
from marubatsu import Marubatsu_GUI
import ipywidgets as widgets

def create_dropdown(self):
元と同じなので省略
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )
+       print(select_values[i])   

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

上記の修正後に下記のプログラムを実行すると、実行結果の __function ai2s__ という表示から __`select_values[i]` に `ai2s` という関数が代入されている__ ことが確認できます。なお、Dropdown は 〇 と × の手番の 2 つを作成し、どちらも `ai2s` を選択状態にするので 2 回表示されます。また、ゲーム盤を表す画像は元と同じなので省略します。

```python
gui_play(ai=[ai2s, ai2s])
```

実行結果（at の右の数字は毎回変わります）

```
<function ai2s at 0x00000205B8D46660>
<function ai2s at 0x00000205B8D46660>
```

Dropdown の `value` 属性に __`ai2s` の関数のみが代入されている__ ことが確認できたので、`create_dropdown` の中で __`select_values[i]` を計算する処理を探す__ と、その処理は下記のプログラムのように記述されていることが確認できます。

下記のプログラムでは、手番の担当が人間の場合は 4 行目で `"人間"` という文字列が、__担当が AI の場合__ は 7 行目で __AI の関数が `value` に代入__ され、9 行目で `select_values` に `value` の値が追加されます。

```python
1  # ラベルと項目の値を計算する
2  if self.mb.ai[i] is None:
3      label = "人間"
4      value = "人間"
5  else:
6      label = self.mb.ai[i].__name__        
7      value = self.mb.ai[i]
8  # value を select_values に常に登録する
9  select_values.append(value)
```

このことから、エラーの原因が、「[前回の記事](https://qiita.com/ysgeso/items/903dc60e865ff56191fc#ai-%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%A8%E3%81%99%E3%82%8B%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E8%A1%A8%E3%81%99%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0)で __Dropdown の項目の値を ( AI の関数, AI のパラメーター ) という tuple に変更__ したにも関わらす、Dropdown に最初に選択される項目の値をそのような __tuple に変更するのを忘れてしまったため__」であることがわかります。

従って、`create_dropdown` を下記のプログラムのように修正することで、このエラーを修正することができます。

* __6 行目__：人間が担当する場合はパラメーターは存在しないのでパラメーターを表す要素に 空の dict を代入した tuple に変更する
* __9 行目__：AI が担当する場合は、パラメーターは `self.params[i]` に代入されているので、それをパラメーターを表す要素に代入した tuple に変更する

```python
 1  def create_dropdown(self):
元と同じなので省略
 2      for i in range(2):
 3          # ラベルと項目の値を計算する
 4          if self.mb.ai[i] is None:
 5              label = "人間"
 6              value = ( "人間", {} )
 7          else:
 8              label = self.mb.ai[i].__name__        
 9              value = ( self.mb.ai[i], self.params[i] )
10          # value を select_values に常に登録する
11          select_values.append(value)
元と同じなので省略
12
13  Marubatsu_GUI.create_dropdown = create_dropdown
```

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

```python
def create_dropdown(self):
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 〇 と × の Dropdown を格納する list
    self.dropdown_list = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
            value = ( "人間", {} )
        else:
            label = self.mb.ai[i].__name__        
            value = ( self.mb.ai[i], self.params[i] )
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value
    
        # Dropdown の description を計算する
        description = "〇" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )
        print(select_values[i])   

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

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

```diff_python
def create_dropdown(self):
元と同じなので省略
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
-           value = "人間"
+           value = ( "人間", {} )
        else:
            label = self.mb.ai[i].__name__        
-           value = self.mb.ai[i]
+           value = ( self.mb.ai[i], self.params[i] )
        # value を select_values に常に登録する
        select_values.append(value)
元と同じなので省略

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

上記の修正後に下記のプログラムを実行すると、実行結果のように __`select_values[i]` に `(ai2s, {})` という tuple が代入されている__ ことが確認できます。また、リセットボタンをクリックしてもエラーが発生しなくなることが確認できます。

```python
gui_play(ai=[ai2s, ai2s])
```

実行結果（at の右の数字は毎回変わります）

```
(<function ai2s at 0x00000205B8D46660>, {})
(<function ai2s at 0x00000205B8D46660>, {})
```

## Dropdown で人間を選択した際のバグ

上記の修正でバグが完全に修正されたと思った人がいるかもしれませんが、実は別のバグがまだ潜んでいます。それは、上記の `gui_play` を実行後に、__どちらかの Dropdown に「人間」を選択してリセットボタンをクリックする__ と、下記のような __エラーが発生する__ というものです。このエラーの原因について少し考えてみて下さい

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File c:\Users\ys\ai\marubatsu\114\marubatsu.py:776, in Marubatsu_GUI.create_event_handler.<locals>.on_reset_button_clicked(b)
    774 self.mb.restart()
    775 self.output.clear_output()
--> 776 on_change_button_clicked(b)

File c:\Users\ys\ai\marubatsu\114\marubatsu.py:767, in Marubatsu_GUI.create_event_handler.<locals>.on_change_button_clicked(b)
    764 for i in range(2):
    765     self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
    766                                             else self.dropdown_list[i].value
--> 767 self.mb.play_loop(self, self.params)

File c:\Users\ys\ai\marubatsu\114\marubatsu.py:397, in Marubatsu.play_loop(self, mb_gui, params)
    395 # ai が着手を行うかどうかを判定する
    396 if ai[index] is not None:
--> 397     x, y = ai[index](self, **params[index])
    398 else:
    399     # キーボードからの座標の入力
    400     coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")

TypeError: 'str' object is not callable
```

### エラーの原因の検証

エラーは下記のプログラムを実行した際に発生し、エラーメッセージは文字列型（str）のオブジェクトは呼び出すことができない（not callable）という意味を表します。

```python
x, y = ai[index](self, **params[index])
```

従って、__`ai[index]`__ に関数ではなく __文字列型のデータが代入されている可能性が高い__ ことがわかります。そこでエラーメッセージをさかのぼって __`ai[index]` がどのように計算されるかを確認__ すると、その処理は下記のエラーメッセージに記述されていることがわかります。

```
File c:\Users\ys\ai\marubatsu\114\marubatsu.py:767, in Marubatsu_GUI.create_event_handler.<locals>.on_change_button_clicked(b)
    764 for i in range(2):
    765     self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
    766                                             else self.dropdown_list[i].value
--> 767 self.mb.play_loop(self, self.params)
```

下記は上記のエラーメッセージから `ai` に値を代入する処理を抜き出したものです。

```python
self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                           else self.dropdown_list[i].value
```

この処理は、`ai[i]` に下記のような値を計算して代入するという処理を意図したものです。

* Dropdown に選択されている項目の値が `"人間"` の場合は `None` を代入する
* そうでなければ __`self.dropdown_list[i].value`__ に代入された __tuple の 最初の要素を代入__ する

一見すると上記のプログラムで正しい処理を行うことができるように見えるかもしれませんが、上記のプログラムは間違っています。どこが間違っているかを少し考えてみて下さい。

間違いは、__`self.dropdown_list[i].value`__ には __tuple が代入されている__ にも関わらず、__`self.dropdown_list[i].value == "人間"`__ という条件式で Dropdown に選択されている項目の値が __`"人間"` であるかを判定している点__ です。

Dropdown に __人間が選択されている場合__ は `self.dropdown_list[i].value` には __`( "人間", {} )` という tuple が代入されている__ ので、条件式は tuple の __最初の要素と `"人間"` を比較__ する __`self.dropdown_list[i].value[0] == "人間"`__ のように記述する必要があります。

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

```python
 1  from marubatsu import Marubatsu
 2  import math
 3  import pickle
 4  from datetime import datetime
 5  from tkinter import Tk, filedialog
 6
 7  def create_event_handler(self):
元と同じなので省略
 8      # 変更ボタンのイベントハンドラを定義する
 9      def on_change_button_clicked(b):
10          for i in range(2):
11              self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
12                                                         else self.dropdown_list[i].value
13          self.mb.play_loop(self, self.params)
元と同じなので省略
14    
15  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

```python
from marubatsu import Marubatsu
import math
import pickle
from datetime import datetime
from tkinter import Tk, filedialog

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"]
                change_step(data["move_count"])
                for i in range(2):
                    value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
                    self.dropdown_list[i].value = value               
                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):
        name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                for i in range(2)]
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{name[0]} VS {name[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,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
                
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

マスの上でクリックすることで着手を行う。
下記の GUI で操作を行うことができる。
()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。

乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
開く(-,L)\tファイルから対戦データを読み込む
保存(+,S)\tファイルに対戦データを保存する
？(*,H)\t\tこの操作説明を表示する
手番の担当\tメニューからそれぞれの手番の担当を選択する
\t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
変更\t\tゲームの途中で手番の担当を変更する
リセット\t手番の担当を変更してゲームをリセットする
待った(0)\t1つ前の自分の着手をキャンセルする
<<(↑)\t\t最初の局面に移動する
<(←)\t\t1手前の局面に移動する
>(→)\t\t1手後の局面に移動する
>>(↓)\t\t最後の着手が行われた局面に移動する
スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する

手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    self.help_button.on_click(on_help_button_clicked)
    
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
                                                       else 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
from marubatsu import Marubatsu
import math
import pickle
from datetime import datetime
from tkinter import Tk, filedialog

def create_event_handler(self):
元と同じなので省略
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
-           self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
+           self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
                                                       else self.dropdown_list[i].value
        self.mb.play_loop(self, self.params)
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、どちらかの担当を「人間」に変更した後でリセットボタンを押した際にエラーが発生しなくなったことを確認して下さい。

```python
gui_play(ai=[ai2s, ai2s])
```

## 修正前のプログラムで上記のエラーが発生しないというバグ

上記で修正した、__Dropdown の選択を人間に選び直した際__ に発生するエラーは、実は今回の記事の __修正を行う前のプログラムでは発生しません__。興味がある方は、__JupyterLab を再起動__ して今回行った修正をなかったことにした後で、下記の手順の作業を行ってみて下さい。__エラーは発生せず、問題なく対戦を行うことができる__ はずです。なお、今回の記事の marubatsu.ipynb では下記の作業は行いません。

* `from util import gui_play` を実行して今回の記事の修正を行う前の `gui_play` をインポートし、`gui_play()` を実行する
* いずれかの Dropdown にいずれかの AI を選択後に変更ボタンをクリックする
* その後で再び Dropdown に __人間選択しなおして__ 変更ボタンをクリックする

`gui_play` で `ai_dict` に代入した __人間の項目に対応するキー__ の値には、下記のプログラムのように __tuple が代入される__ ので、上記の操作で Dropdown に __人間を選択しなおす__ と、__Dropdown の `value` 属性には `("人間", {})` という tuple が代入されるはず__ です。

```python
ai_dict = { "人間": ("人間", {}) }
```

そのため、今回の記事で先程行った __修正を行う前のプログラム__ では、__変更ボタンをクリックした際に行われる__ 下記の処理で __エラーが発生するはず__ なのですが、__実際にはエラーは発生しません__。何故エラーが発生しないかについて少し考えてみて下さい。

```python
self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                           else self.dropdown_list[i].value
```

:::note warn
なお、エラーが発生しないことは良いことであると思う人がいるかもしれませんが、__その考え方は間違っています__。__発生するべきエラーが発生しない__ という現象は、エラーが発生するよりも __良くないことです__。

例えば、痛みは体の中の不調を知らせるものです。体の中に不調があるにも関わらず、__痛みを感じない__ ということは、__不調の存在を見過ごす__ ことになってしまうので決して __良いことであるとは言えません__。

それと同様に、__バグが存在するにもかかわらず__、そのことが __エラーなどで明るみに出ない__ ということは、__決して良いことではないのです__。

病気を早期に発見して対処することが重要であるのと同様に、プログラムのバグも __なるべく早く発見して対処することが重要__ です。病気と同様に、バグを見過ごしてしまうと、__後でそのバグが発見された時の修正の作業が大変__ になるからです。

残念ながらそのような症状がなかなか明るみに出ないバグは、よくあるバグの一つで、一般的に発見と修正が困難です。このバグを詳しく説明しているのは、バグの発見と修正の技術の向上には様々なバグを経験することが重要だからです。
:::
### エラーが発生しない原因の検証

エラーが発生しない原因は、`create_dropdown` 内の下記のプログラムにあります。なお、下記は __今回の記事で修正を行う前__ のプログラムである点に注意して下さい。

:::note
忘れている人が多いと思いますのでおさらいしますが、__13 ~ 15 行目の処理__ は、`gui_play` を呼び出す際に __キーワード引数 `ai` を記述して指定した AI が__、__Dropdown の項目に含まれていない場合__ に、その AI を Dropdown の __項目に登録するための処理__ です。例えば `gui_play` は `ai2s` のように、ルールベースの AI に関しては名前の最後に `s` がつく AI を Dropdown に登録しますが、`ai1` のような `s` がつかない AI は登録しません。13 ~ 15 行目は `gui_play(ai=[ai1, ai1])` のように、`s` がつかない AI で対戦を行った場合に、Dropdown に `ai1` を登録する処理です。
:::

```python
 1  def create_dropdown(self):
元と同じなので省略
 2      for i in range(2):
 3          # ラベルと項目の値を計算する
 4          if self.mb.ai[i] is None:
 5              label = "人間"
 6              value = "人間"
 7          else:
 8              label = self.mb.ai[i].__name__        
 9              value = self.mb.ai[i]
10          # value を select_values に常に登録する
11          select_values.append(value)
12          # value が ai_values に登録済かどうかを判定する
13          if value not in self.ai_dict.values():
14              # 項目を登録する
15              self.ai_dict[label] = value
略
```

キーワード引数 `ai` を記述せずに `gui_play()` を実行した場合は、下記の手順で処理が行われます。


* `gui_play()` のように キーワード引数 `ai` を記述せずに呼び出した場合は、最初に 人間 VS 人間 の対戦が行われる
* 人間が担当する場合は `self.mb.ai[i]` には `None` が代入されるので、6 行目が実行されて `label` と `value` に `"人間"` という文字列が代入される
* 13 行目で、`self.ai_dict` のキーの値に `value` に代入された `"人間"` が存在しないことを判定する
* `ai_dict` の `"人間"` というキーに対応するキーの値は `( "人間", {} )` という tuple なので、`"人間"` というキーの値は存在しない。
* 13 行目の条件式が `True` になるので、15 行目が実行され、__`ai_dict` の `"人間"` のキーの値__ が `( "人間", {} )` から __`"人間"` に上書きされる__

上記の処理が行われた結果、Dropdown に __人間を選択すると__ に __`dropdown_list[i].value`__ には `( "人間", {} )` ではなく、__`"人間"` が代入されるようになります__。

```python
self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                           else self.dropdown_list[i].value
```

そのため、上記のプログラムが実行された際には、下記のプログラムの処理が実行され、`self.mb.ai[i]` には人間が担当することを表す `None` が、`self.params[i]` にはパラメーターが存在しないことを表す `{}` が正しく代入されます。

```python
self.mb.ai[i], self.params[i] = (None, {})
```

### このバグの性質のまとめと注意点

以上が、今回の記事の最初で説明したバグが存在するにも関わらず、正しい処理が行われてしまう理由ですが、このことを別の言葉で説明すると以下のようになります。

* プログラムに __バグが 2 つ存在する[^1]__
* それぞれのバグによって間違った処理が行われる
* それぞれのバグを バグ A と バグ B と表記すると、バグ A によって行われた間違った処理の結果を使ってバグ B が間違った処理を行った結果、__偶然正しい処理が行われてしまった__

まだわかりづらいかもしれませんので、現実の例で例えると以下のようになります。

* 直方体の体積を計算する際に、底面の長方形の面積を間違って「縦 × 横 ÷ 2」という式で計算した[^2]
* 体積を計算するために間違って「底面の面積 × 高さ × 2」で計算した[^3]

上記の手順で行われた 2 つの計算は、いずれも間違っていますが、__2 つの間違いを重ねた結果__、__偶然に正しく__ 直方体の体積が __計算されてしまいます__。

途中の計算が間違っていても、正しい答えが得られるのだから結果オーライだと思う人がいるかもしれませんが、例えば同じプログラムの中で、上記の間違った長方形の面積の計算方法を使って、直方体の表面積を計算すると __間違った答えが計算されてしまいます__。このようなバグを放置すると、その場はうまく行くように見えるかもしれませんが、__後で予期しない別のバグが発生する原因になる__ ので、__決して放置するべきではありません__。

:::note warn
このように、__複数のバグが行った処理__ によって __偶然正しい処理が行われてしまう__ というバグはかなり質の悪いバグで、バグの発見や修正が困難です。今回の記事で示したように __実際に発生する場合がある__ ので紹介しました。
:::

なお、JupyterLab を再起動した方は、もう一度今回の記事で入力したバグを修正したプログラムを実行しなおしておいてください。

## 人間を担当する際のデータ構造の修正

手番の担当が人間であるかどうかによって、下記のプログラムのような処理を行うのがわかりづらく、面倒だと思った人はいないでしょうか。

```python
self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
                                           else self.dropdown_list[i].value
```

[かなり前の記事](https://qiita.com/ysgeso/items/64de676735836b963712#dropdown-%E3%81%AE-value-%E5%B1%9E%E6%80%A7%E3%81%AE-none-%E3%81%AE%E6%84%8F%E5%91%B3)で説明したので忘れた方が多いのではないかと思いますが、このような処理を行っているのは、選択中の Dropdown の項目の値を表す __`value` 属性に `None` が代入されている場合__ は、__どの項目も選択されていないという意味を表す__ ため、人間の項目の値に `None` を設定することができず、`None` に代わる別の値として `"人間"` という文字列を設定するという苦肉の策を行ったからです。忘れた方は復習して下さい。

しかし、前回の記事で Dropdown の __項目の値__ を __( AI の関数, AI のパラメーター )__ という __tuple に変更した__ ので、__人間を表す項目の値__ を `( "人間", {} )` ではなく、__`( None, {} )`__ のように __変更することができる__ ようになっています。また、そのように変更することで、上記のプログラムを下記のように __簡潔に記述__ することができるようになります。

```python
self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
```

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

```diff_python
-self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
-                                           else self.dropdown_list[i].value
+self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
```
</details>

そこで、人間が担当する際のデータ構造を `( "人間", {} )` という tuple から `( None, {} )` という tuple に変更することにします。

### `gui_play` の修正

下記は `gui_play` の修正です。

* __7 行目__：人間の項目に対応する項目の値を `( None, {} )` に修正する

```python
1  import ai as ai_module
2  from ai import ai_gt6
3  from util import load_bestmoves
4
5  def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
6      if ai_dict is None:
7          ai_dict = { "人間": ( None, {} ) }
元と同じなので省略
```

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

```python
import ai as ai_module
from ai import ai_gt6
from util import load_bestmoves

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    if params is None:
        params = [{}, {}]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": ( None, {} )}
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = (getattr(ai_module, ai_name), {})
        bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
        ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
    
    print(ai_dict)
    mb = Marubatsu()
    mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)
```
</details>

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

```diff_python
import ai as ai_module
from ai import ai_gt6
from util import load_bestmoves

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
    if ai_dict is None:
-       ai_dict = { "人間": ( "人間", {} ) }
+       ai_dict = { "人間": ( None, {} ) }
元と同じなので省略
```
</details>

### `create_dropdown` の修正

下記は `create_dropdown` の修正です。

* __4 ~ 8 行目__：人間が担当する場合の項目の値を `( None, {} )` に変更したことで、__項目の値は__ 人間でも AI でも __同じ `( self.mb.ai[i], self.params[i] )` で表現できるようになった__ ので、項目の値を計算する処理を  if 文の後の 8 行目に移動する
* 今回の記事で先程デバッグのために 9 行目に記述した `print(select_values[i])` はもう必要がないので削除した

```python
 1  def create_dropdown(self):
元と同じなので省略
 2      for i in range(2):
 3          # ラベルと項目の値を計算する
 4          if self.mb.ai[i] is None:
 5              label = "人間"
 6          else:
 7              label = self.mb.ai[i].__name__
 8          value = ( self.mb.ai[i], self.params[i] )
元と同じなので省略
 9
10  Marubatsu_GUI.create_dropdown = create_dropdown
```

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

```python
def create_dropdown(self):
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 〇 と × の Dropdown を格納する list
    self.dropdown_list = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
        else:
            label = self.mb.ai[i].__name__        
        value = ( self.mb.ai[i], self.params[i] )
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value
    
        # Dropdown の description を計算する
        description = "〇" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )

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

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

```diff_python
def create_dropdown(self):
元と同じなので省略
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
-           value = ( None, {} )
        else:
            label = self.mb.ai[i].__name__        
-           value = ( self.mb.ai[i], self.params[i] )
+       value = ( self.mb.ai[i], self.params[i] )
元と同じなので省略
-       print(select_values[i])   

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

### `create_event_handler` の修正

下記は `create_event_handler` の修正です。

* __5 行目__：先ほど説明したように、if 文を使わずに、直接 `ai` と `params` に Dropdown の `value` 属性に代入されている tuple の各要素の値を代入するように修正する

```python
1  def create_event_handler(self):
元と同じなので省略
2      # 変更ボタンのイベントハンドラを定義する
3      def on_change_button_clicked(b):
4          for i in range(2):
5              self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
6          self.mb.play_loop(self, self.params)
元と同じなので省略
7     
8  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

```python
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"]
                change_step(data["move_count"])
                for i in range(2):
                    value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
                    self.dropdown_list[i].value = value               
                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):
        name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                for i in range(2)]
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{name[0]} VS {name[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,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
                
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

マスの上でクリックすることで着手を行う。
下記の GUI で操作を行うことができる。
()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。

乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
開く(-,L)\tファイルから対戦データを読み込む
保存(+,S)\tファイルに対戦データを保存する
？(*,H)\t\tこの操作説明を表示する
手番の担当\tメニューからそれぞれの手番の担当を選択する
\t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
変更\t\tゲームの途中で手番の担当を変更する
リセット\t手番の担当を変更してゲームをリセットする
待った(0)\t1つ前の自分の着手をキャンセルする
<<(↑)\t\t最初の局面に移動する
<(←)\t\t1手前の局面に移動する
>(→)\t\t1手後の局面に移動する
>>(↓)\t\t最後の着手が行われた局面に移動する
スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する

手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    self.help_button.on_click(on_help_button_clicked)
    
    # 変更ボタンのイベントハンドラを定義する
    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
def create_event_handler(self):
元と同じなので省略
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
-           self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
-                                                      else self.dropdown_list[i].value
+           self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
        self.mb.play_loop(self, self.params)
元と同じなので省略
     
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

### `update_gui` の修正

下記は `update_gui` の修正です。なお、元のプログラムでは、if 文を使って対戦カードの文字列を計算していましたが、よく考えると __Dropdown__ に選択されている __項目の名前をそのまま対戦カードに表示すればよい__ ことに気が付きましたので、そのように修正しました。なお、Dropdown に __選択中の項目の名前__ は、__`label` 属性に代入__ されています。

* __2 行目__：Dropdown の `label` 属性を使って対戦カードを表示するように修正する

```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}", fontsize=20, ha="center")   
元と同じなので省略
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=20, 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=20)
    
    self.draw_board(ax, self.mb)
    
    self.update_widgets_status() 
    
Marubatsu_GUI.update_gui = update_gui
```
</details>

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

```diff_python
def update_gui(self):
元と同じなので省略
-   names = []
-   for i in range(2):
-       names.append("人間" if ai[i] is None else ai[i].__name__)
-   ax.text(1.5, 3.5, f"{names[0]}　VS　{names[1]}", fontsize=20, ha="center")   
+   ax.text(1.5, 3.5, f"{self.dropdown_list[0].label}　VS　{self.dropdown_list[1].label}", fontsize=20, ha="center")   
元と同じなので省略

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

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、様々な操作を行って問題なく対戦を行うことができることを確認して下さい。

```python
gui_play()
```

また、下記のプログラムでも確認を行ってください。

```python
gui_play(ai=[ai2s, ai2s])
```

## Dropdown に登録されていない AI の登録の改良

致命的なバグではありませんが、下記のプログラムのようにキーワード引数 `ai` に __Dropdown に登録されていない__ `ai1` と `ai2` を記述して `gui_play` を実行すると、実行結果の左図のように __〇 の手番の Dropdown__ に __`ai1` しか登録されない__ というバグが発生しますす。なお、実行結果の右図のように __× の 手番の Dropdown には両方の AI が登録__ されます。このバグの原因について少し考えてみて下さい。

```python
from ai import ai1, ai2

gui_play(ai=[ai1, ai2])
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3c9618a6-0078-3403-53d6-172813a2cbc8.png" width="150">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/612eaab6-c7ad-c0b3-127c-cc30d7a0b954.png" width="140">

このようなことが起きる原因は、`gui_play(ai=[ai1, ai2])` を実行すると、下記の `create_dropdown` に対して、以下のような手順で処理が行われるからです。

1. 2 ~ 13 行目の 1 回目の繰り返し処理によって、`ai1` に対する処理が行われる
2. `ai1` は　`self.ai_dict` のキーの値に存在しないので、5 行目が実行されて `ai_dict` に `ai1` に対するキーとキーの値が代入される
3. 10 ~ 12 行目で __`ai1` が追加して登録された__ Dropdown が作成されるが、__`ai2` に対する処理はまだ行われていない__ ので、作成した Dropwdown には __`ai2` の項目は存在しない__
4. 2 ~ 13 行目の 2 回目の繰り返し処理によって、`ai2` に対する処理が行われる
5. `ai2` は `self.ai_dict` のキーの値に存在しないので、5 行目が実行されて `ai_dict` に `ai2` に対するキーとキーの値が代入される。その際に `ai_dict` には __`ai1` のキーとキーの値は代入済__ である
6. 10 ~ 12 行目で __`ai1` と `ai2` が追加して登録された__ Dropdown が作成される

```python
 1  def create_dropdown(self):
略
 2       for i in range(2):
略
 3           if value not in self.ai_dict.values():
 4               # 項目を登録する
 5               self.ai_dict[label] = value
 6
 7           # Dropdown の description を計算する
 8           description = "〇" if i == 0 else "×"
 9           self.dropdown_list.append(
10               widgets.Dropdown(
11                   options=self.ai_dict,
略
12               )
13           )
```

従って、この問題は下記のプログラムのように、`ai_dict` に __`ai1` と `ai2` の両方を登録した後で、Dropdown を作成する__ ように修正することで解決することができます。

* __7 行目__：__改めて for 文を記述する__ ことで、2 ~ 5 行目の for 文の処理によって `ai_dict` に項目を登録する __処理が完了してから__、__2 つの Dropdown を作成する__ ように修正する

```python
 1  def create_dropdown(self):
元と同じなので省略
 2      for i in range(2):
元と同じなので省略
 3          if value not in self.ai_dict.values():
 4              # 項目を登録する
 5              self.ai_dict[label] = value
 6
 7      for i in range(2):
 8          # Dropdown の description を計算する
 9          description = "〇" if i == 0 else "×"
10          self.dropdown_list.append(
元と同じなので省略
11
12  Marubatsu_GUI.create_dropdown = create_dropdown
```

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

```python
def create_dropdown(self):
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 〇 と × の Dropdown を格納する list
    self.dropdown_list = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
        else:
            label = self.mb.ai[i].__name__        
        value = ( self.mb.ai[i], self.params[i] )
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value

    for i in range(2):
        # Dropdown の description を計算する
        description = "〇" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )

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

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

```diff_python
def create_dropdown(self):
元と同じなので省略
    for i in range(2):
元と同じなので省略
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value

+   for i in range(2):
        # Dropdown の description を計算する
        description = "〇" if i == 0 else "×"
        self.dropdown_list.append(
元と同じなので省略

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

上記の修正後に下記のプログラムを実行すると、実行結果のように 〇 の手番の Dropdown にも `ai2` が登録されるようになったことが確認できます。

```python
gui_play(ai=[ai1, ai2])
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3d9737b1-165f-d4b1-fb5e-ea4c2e499129.png" width="150">　

# 今回の記事のまとめ

今回の記事では、データ構造を修正した結果生じた Marubatsu_GUI クラスのバグを修正し、いくつかの改良を行いました。なお、紹介したバグの中で、__複数のバグが行った処理__ によって __偶然正しい処理が行われてしまう__ というバグは __実際に悩まされることが多いバグです__。

これでおそらくゲームを遊ぶという観点での Marubatsu_GUI クラスのバグは修正されたのではないかと思いますが、100 % バグが存在しないかどうかまではわからないので、何かバグが見つかればその都度修正しようと思います。また、何らかのバグを発見した方はコメントで指摘して頂ければ助かります。

なお、ゲームを遊ぶという観点以外では、ゲームの保存と読み込みに関するバグが残っているので、次回の記事で修正することにします。余裕がある方はどのようなバグがあるかについて調べてみて下さい。



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

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

# 次回の記事

https://qiita.com/ysgeso/items/53147dbb56f83be32edc

[^1]:バグが 3 つ以上存在する場合も同様です
[^2]:正しい長方形の面積の式は「縦 × 横」です
[^3]:正しい直方体の体積の式は「底面の面積 × 高さ」です