Pythonで〇×ゲームのAIを一から作成する　その85　キー入力による着手の処理の実装と inspect モジュールを利用したバグの修正

# 目次と前回の記事

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

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

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

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

https://github.com/ysgeso/marubatsu/blob/master/085/marubatsu.py

https://github.com/ysgeso/marubatsu/blob/master/085/ai.py

https://github.com/ysgeso/marubatsu/blob/master/085/util.py

## ルールベースの AI の一覧

ルールベースの AI の一覧については、下記の記事を参照して下さい。

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

# キー入力による操作（続き）

前回の記事では、下記のキー入力による〇×ゲームの操作の実装のうち、着手を行う 1 ~ 9 以外のキー操作に対応する処理の実装を行いました。

| キー | 行われる操作 |
|:-:|:--|
| 1 ~ 9  | テンキーをゲーム盤のマスと見立て、対応するマスに着手を行う  |
| ↑  | ゲーム開始時まで戻す（<< ボタンに対応） |
| ←  | 一手戻す（< ボタンに対応） |
| →  | 一手進める（> ボタンに対応） |
| ↓  | 最後に行われた着手まで進める（>> ボタンに対応） |
| 0  | 待ったを行う  |
| Enter  | ゲームをリセットする  |

## 着手を行うキーの座標の計算方法

残りの __1 ~ 9 のキーが押された時に着手を行う処理__ を実装します。まず、__押されたキーから__ 着手を行う __ゲーム盤のマスの x、y 座標を計算__ する必要があります。どのような式で計算できるかについて少し考えてみて下さい。

計算方法が分からない人は、[以前の記事](https://qiita.com/ysgeso/items/1a8a4319ef3c896ef271#%E6%95%B0%E5%80%A4%E5%BA%A7%E6%A8%99)で説明した __数値座標__ を思い出してください。以前の記事では、下図のようにそれぞれのマスに 0 ~ 8 までの整数の数値座標を割り当て、その __整数から x、y 座標を計算する方法__ を紹介しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1ce26cde-7d47-4b8c-27d2-f6a14a1cd1ca.png" width="150">

この数値座標から x、y 座標への変換は下記の計算で行うことができます。繰り返しになるので、下記の計算方法の詳細については[以前の記事](https://qiita.com/ysgeso/items/1a8a4319ef3c896ef271#num_to_xy-%E3%81%AE%E5%AE%9A%E7%BE%A9%E3%81%9D%E3%81%AE-3-%E8%A8%88%E7%AE%97%E3%82%92%E4%BD%BF%E3%81%86%E6%96%B9%E6%B3%95)を復習して下さい。

* __x 座標__：数値座標を 3 で割った余りを % 演算子で計算する
* __y 座標__：数値座標を 3 で割った商を // 演算子で計算する

下記の `num_to_xy` は[以前の記事](https://qiita.com/ysgeso/items/1a8a4319ef3c896ef271#num_to_xy-%E3%81%AE%E5%AE%9A%E7%BE%A9%E3%81%9D%E3%81%AE-3-%E8%A8%88%E7%AE%97%E3%82%92%E4%BD%BF%E3%81%86%E6%96%B9%E6%B3%95)で定義した、仮引数 `coord` に代入された数値座標から x、y 座標を計算して返す関数です。

```python
def num_to_xy(coord):
    x = coord % 3
    y = coord // 3
    return x, y
```

上記を参考に、押されたキーの数字から x、y 座標を計算する方法を考えることにします。以前の記事では 0 から 8 までの整数をそれぞれのマスに割り当てましたが、__今回の場合は 1 から 9 の整数が割り当てられています__。押された __キーの数字から 1 を引く__ ことで、1 から 9 までの整数が __0 から 8 までの整数__ になるので、__先程の数値座標と同じ範囲の数値__ になります。下図左は先ほどと同じ図で、__下図右は押されたキーの数字から 1 を引いたもの__ です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1ce26cde-7d47-4b8c-27d2-f6a14a1cd1ca.png" width="150">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/899592fa-ff50-fc7d-ac00-0044d5e07757.png" width="150">

図を比べると、各数字に対応するマスの __x 座標は同じ__ であることがわかります。従って、押されたキーの数字を `num` とすると、__x 座標を以下の式で計算__ することができます。

```python
x = (num - 1) % 3  # 数値座標から 1 を引いた値を 3 で割った余りを計算する
```

y 座標に関しては、以下の表のような違いがあります。

| 数値座標 | 上図左の y 座標 | 上図右の y 座標 |
|:-:|:-:|:-:|
| __0 ~ 2__  | 0  | 2  |
| __3 ~ 5__  | 1  | 1  |
| __6 ~ 8__  | 2  | 0  |

求める y 座標は、上図左の y 座標に対して __0 → 2、1 → 1、2 → 0 という変換を行う式__ で計算することができます。この矢印の __左右の数値の合計はすべて 2 になる__ ので、矢印の __右の数字は 2 から左の数字を引き算する__ ことで計算できます。従って、押されたキーの数字を __`num`__ とすると、__y 座標を以下の式で計算__ することができます。

```python
num -= 1      # 押されたキーの数値から 1 を引いて上図右の整数に変換する
y = num // 3  # num を 3 で割った商を計算し、上図左の y 座標を計算する 
y = 2 - y     # 2 から y を引き算することで、上図右の y 座標を計算する
```

下記は、上記を 1 つの式にまとめたプログラムです。

```python
y = 2 - ((num - 1) // 3)  
```

## 押されたキーに対応する座標の表示

上記の式が正しいことを確認するために、1 ~ 9 のキーが押された場合に、着手を行うマスの x、y 座標を計算して表示する処理を、下記のプログラムのように実装します。

* __8 行目__：リセットボタンなどに対応するキーが押されていないことを判定する
* __9 行目__：__`event.key`__ に代入されているのは __文字列型のデータ__ なので、割り算などを計算する際は、組み込み関数 __`int` を使って整数型のデータに変換する必要がある__。また、その際に __この後の計算で必要となる 1 を引く計算を行っておく__
* __10、11 行目__：先ほどの式を使って x、y 座標を計算する
* __12 行目__：計算した x、y 座標を `print` で表示する

```python
 1  from marubatsu import Marubatsu, Marubatsu_GUI
 2  import math
 3
 4  def create_event_handler(self):
元と同じなので省略
 5      def on_key_press(event):
元と同じなので省略
 6          if event.key in keymap:
 7              keymap[event.key]()
 8          else:
 9              num = int(event.key) - 1
10              x = num % 3
11              y = 2 - (num // 3)
12              print(x, y)
元と同じなので省略
13    
14  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

```python
from marubatsu import Marubatsu, Marubatsu_GUI
import math

def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        self.mb.restart()
        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.draw_board()
        
    # イベントハンドラをボタンに結びつける
    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.draw_board()        

    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):
        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)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    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,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            num = int(event.key) - 1
            x = num % 3
            y = 2 - (num // 3)
            print(x, y)
            
    # 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, Marubatsu_GUI
import math

def create_event_handler(self):
元と同じなので省略
    def on_key_press(event):
元と同じなので省略
        if event.key in keymap:
            keymap[event.key]()
+       else:
+           num = int(event.key) - 1
+           x = num % 3
+           y = 2 - (num // 3)
+           print(x, y)
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

上記の修正後に、下記のプログラムで `gui_play` を実行し、__ゲーム盤の画像の上でマウスをクリックして Figure を選択後__ に 1 ~ 9 キーを押す と、実行結果のように、__押したキーに対応するマスの x、y 座標が表示__ されます。下記の実行結果は 1 ~ 9 の順番でキーを押した場合のものです。なお、ボタンやゲーム盤の画像は以前と同じなので省略します。

```python
from util import gui_play

gui_play()
```

実行結果

```
0 2
1 2
2 2
0 1
1 1
2 1
0 0
1 0
2 0
```

上記のプログラムには大きな問題があります。それが何かを少し考えてみて下さい。

## 数値以外のキーを押した場合の対処

上記のプログラムは、__数字以外のキーを押すとエラーが発生する__ という問題があります。例えば a キーを押すと下記のようなエラーが発生します。

```
略
Cell In[1], line 80
     78     keymap[event.key]()
     79 else:
---> 80     num = int(event.key) - 1
     81     x = num % 3
     82     y = 2 - (num // 3)

ValueError: invalid literal for int() with base 10: 'a'
```

このエラーは、組み込み関数 __`int`__ によって、__整数に変換することができない__ `"a"` という文字列を __整数に変換しようとしたことが原因__ です。この問題は、[以前の記事](https://qiita.com/ysgeso/items/1800a353cb609a0fef4e#%E4%BE%8B%E5%A4%96%E5%87%A6%E7%90%86)で説明した __例外処理__ を使って対処するのが最も簡単でしょう。例外処理は、__エラーが発生する可能性がある処理__ を __`try` のブロックの中に記述__ することで、__エラーが発生した場合__ は __プログラムを停止せず__ に __`except` のブロックの処理を実行する__ というものです。

これまでのプログラムで利用した例外処理は、__`play`__ メソッドで __キーボードから入力した文字列で指定された座標__ に着手を行う際に、下記のプログラムのように記述することで、__`x` と `y`__ に __整数に変換できない文字列が代入__ されていた場合に、__プログラムを停止する代わり__ に、"整数の座標を入力して下さい" という __エラーメッセージを表示する__ というものです。

```python
try:
    self.move(int(x), int(y))
except:
    print("整数の座標を入力して下さい")
```

下記は例外処理を利用するように `create_event_handler` を修正したプログラムです。

* __5 ~ 9 行目__：1 ~ 9 のキーが押された場合の処理を `try` のブロック内に記述する
* __10、11 行目__：`try` のブロック内でエラーが発生した場合の例外処理を行う `except` のブロックを記述する。1 ~ 9 以外のキーが押された場合にメッセージを表示するのは見た目が煩わしいので、ブロックの中で何の処理も行わないことを表す `pass` を記述している

```python
 1  def create_event_handler(self):
元と同じなので省略
 2          if event.key in keymap:
 3              keymap[event.key]()
 4          else:
 5              try:
 6                  num = int(event.key) - 1
 7                  x = num % 3
 8                  y = 2 - (num // 3)
 9                  print(x, y)
10              except:
11                  pass
元と同じなので省略
12    
13  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

```python
def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        self.mb.restart()
        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.draw_board()
        
    # イベントハンドラをボタンに結びつける
    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.draw_board()        

    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):
        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)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    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,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                print(x, y)
            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):
元と同じなので省略
        if event.key in keymap:
            keymap[event.key]()
        else:
-           num = int(event.key) - 1
-           x = num % 3
-           y = 2 - (num // 3)
-           print(x, y)
+           try:
+               num = int(event.key) - 1
+               x = num % 3
+               y = 2 - (num // 3)
+               print(x, y)
+           except:
+               pass
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play を実行し、1 ~ 9 以外のキーを押した場合にエラーが発生しなくなったことを確認して下さい。

```python
gui_play()
```

## 押されたキーに対応する着手を行う処理の実装

[前回の記事](https://qiita.com/ysgeso/items/17c74178449148ad3ef7#-%E3%82%AD%E3%83%BC%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B%E5%87%A6%E7%90%86%E3%81%AE%E8%A8%98%E8%BF%B0%E6%96%B9%E6%B3%95)では、下記のプログラムのように、一手戻す処理などの __キーが押された場合の処理__ を、__対応する処理__ を行うボタンがクリックされた場合に呼び出される __イベントハンドラを直接呼び出すことで実装__ しました。

```python
def on_key_press(event):
    if event.key == "left":
        on_prev_button_clicked(None)
```

:::note
[前回の記事](https://qiita.com/ysgeso/items/17c74178449148ad3ef7#%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E6%94%B9%E8%89%AF)では、__`on_key_press`__ の処理を改良したため、上記のようなプログラムではなくなっていますが、__`on_key_press`__ が行う処理に変わりはありません。
:::

GUI で __マウスをクリックして着手を行う処理は__ 、下記のプログラムのように、`create_event_handler` 内で __ローカル関数として定義された `on_mouse_down` で行われます__。

```python
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
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)
        self.mb.move(x, y)                
        self.draw_board()
        # 次の手番の処理を行うメソッドを呼び出す
        self.mb.play_loop(self)
```            

そこで、1 ~ 9 のキーが押された場合に着手を行う処理も、上記の `on_mouse_down` を呼び出すことで行うことにします。そのようにすることで、__GUI で着手を行う処理__ を __一か所にまとめる__ ことができるという利点が得られます。

:::note
GUI で x、y のマスに着手する処理を行う __別の関数を定義__ し、`on_mouse_down` と `on_key_press` からその関数を呼び出すという方法もあります。
:::

上記のプログラムから、__`on_mouse_down`__ では、__`event.inaxes` が `True` の場合__ に、__x、y 座標が `event.xdata`、`event.ydata` のマスに着手__ を行います。マウスをクリックした場合のイベントハンドラの __仮引数 `event` に代入されたオブジェクト__ は、__イベントハンドラ以外では使われない__ ので __その属性の値を変更してもかまいません__。従って、下記のようにプログラムを修正することで、1 ~ 9 のキーが押された場合に着手を行う処理を行うことができます。

* __8 行目__：`event.inaxes` に `True` を代入する
* __9、10 行目__：`event.xdata` と `event.ydata` に、計算した x、y 座標を代入する
* __11 行目__：`on_mouse_down(event)` を呼び出して着手を行う

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_key_press(event):
元と同じなので省略
 3          if event.key in keymap:
 4              keymap[event.key]()
 5          else:
 6              try:
 7                  num = int(event.key) - 1
 8                  event.inaxes = True
 9                  event.xdata = num % 3
10                  event.ydata = 2 - (num // 3)
11                  on_mouse_down(event)
12              except:
13                  pass
元と同じなので省略            
14    
15  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

```python
def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        self.mb.restart()
        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.draw_board()
        
    # イベントハンドラをボタンに結びつける
    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.draw_board()        

    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):
        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)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    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,            
        }
        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_key_press(event):
元と同じなので省略
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
+               event.inaxes = True
-               x = num % 3
+               event.xdata = num % 3
-               y = 2 - (num // 3)
+               event.ydata = 2 - (num // 3)
-               print(x, y)
+               on_mouse_down(event)
            except:
                pass
元と同じなので省略            
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

:::note
仮引数 `event` の属性の値を変更してよいかどうかの判断がつかない場合は、下記のプログラムのように、__自分で `inaxes`、`xdata`、`ydata` 属性を持つ dict を作成__ して `on_mouse_down` の実引数に記述するという方法があります。

```python
try:
    num = int(event.key) - 1
    evt = {
        "inaxes": True,
        "xdata": num % 3,
        "ydata": 2 - (num // 3),
    }
    on_mouse_down(evt)
```
:::

実行結果は省略しますが、上記の修正後に、下記のプログラムで `gui_play` を実行し、1 ~ 9 キーによって着手を行うことができることを確認して下さい。

```python
gui_play()
```

# AI と対戦した場合に発生するバグの検証とその対処法

上記の実装後に、確認のため様々な操作を行って、プログラムが正しく動作するかを検証した所、__AI と対戦した場合にバグが発生する__ ことに気が付きました。


具体的には 下記のプログラムで、人間 VS `ai1s` の対戦を開始し、__2 回着手を行う__ と、下図のように __2 回目の着手が画面に表示されず、リプレイ中であることを表す水色の画面になります__。なお、下図は 1 回目で (1, 1) に、2 回目に (2, 2) に着手を行った場合のものです。

```python 
from ai import ai1s

gui_play(ai=[None, ai1s])
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3c4a5a55-aaf8-3797-de7e-6c73a5f8eb01.png" width="200">

また、上図で __> や >> ボタンをクリック__ すると、下図のように __2 回目の着手後の局面が表示される__ ので、__2 回目の着手は確かに行われている__ ことが確認できますが、__その後の `ai1s` の着手が行われていない__ ことがわかります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/f03263f7-8683-69a1-2b07-2b12f4769b90.png" width="200">

このバグは、__原因がわかれば修正は簡単__ に行えますが、バグの原因を見つけることは、初心者には難しいかもしれません。また、バグの原因がわかった場合でも、__1 回目の着手では不具合が起きない__[^1] のに、__2 回目の着手ではバグが発生__ する原因を __正しく理解することはかなり困難__ です。そこで、筆者がこのバグの原因を見つけた手順とバグの原因について詳しく解説することにします。初心者にとっては意味がわかりづらいかもしれませんが、バグの修正を行う方法の一つとして参考になるのではないかと思います。

## `draw_board` 内での `move_count` の値の表示

2 回目の着手を行った際に、表示される __局面の手数が変化しない__ ので、手数を表す __`move_count` 属性の値がおかしい__ ことが推測されます。そこで、__着手を行った際__ に __`move_count` 属性の値__ がどのようになっているかを __確認するため__ に、下記のプログラムの 2 行目のように、__ゲーム盤を描画__ する __`draw_board`__ が呼び出された際に、__`move_count` 属性の値を表示__ するようにします。

```python
def draw_board(self):
    print("draw_board. move_count =", self.mb.move_count)
元と同じなので省略
    
Marubatsu_GUI.draw_board = draw_board
```

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

```python
def draw_board(self):
    print("draw_board. move_count =", self.mb.move_count)
    
    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)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]}　VS　{names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    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)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

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

```diff_python
def draw_board(self):
+    print("draw_board. move_count =", self.mb.move_count)
元と同じなので省略
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

ゲーム盤の画像は省略しますが、上記の修正後に下記のプログラムを実行すると、ゲーム開始時の局面を表示するために __`draw_board` が 1 回呼び出され、`move_count` の値が 0 である__ ことが確認できます。__この動作は特に問題はない__ でしょう。

```python
gui_play(ai=[None, ai1s])
```

実行結果

```
draw_board. move_count = 0
```

次に、(1, 1) のマスをクリックして着手を行うと、下記のような表示が追加されます。

```
draw_board. move_count = 1
draw_board. move_count = 1
draw_board. move_count = 1
draw_board. move_count = 2
```

表示から、__`draw_board`__ が __4 回呼び出され__、__最初の 3 回は `move_count` の値が `1`__ で、__最後の 1 回は `move_count` の値が `2`__ になっていることがわかります。

## inspect モジュールの `stack` 関数による呼び出し元の関数の表示

__`draw_board`__ はプログラムの __様々な場所から呼び出されています__ が、上記のメッセージからは、`draw_board` が __どこから呼び出されたかはわかりません__。プログラムの __処理の流れを辿る__ 際には、関数が __どこから呼び出されたかの情報が表示されると便利__ です。そこで `draw_board` がどこから呼び出されたかの情報を表示することにします。

関数がどこから呼び出されたかは __inspect__ という組み込みモジュールの __`stack` という関数__ で調べることができます。`stack` は、__`stack` を実行した行__ のプログラムが、__どこから呼び出されたか__ の情報を取得する関数で、以下の処理を行います。

* __呼び出した関数の情報__ が __新しい順__ に要素に代入された __list の形式__ で返す
* ただし、__0 番の要素__ には、__その行が記述されている関数の情報__ が代入される
* 呼び出した __関数の名前__ は、それぞれの要素の __`function` という属性に代入__ される[^2]
* __関数ではない場所__ から呼び出した場合は、__モジュールから呼び出された__ ことになるので、__モジュールの情報が代入__ される

`stack` を利用することで、エラーが発生した際に表示される __エラーメッセージのように、処理の流れを辿る__ ことができます。

:::note
stack（スタック）とは、複数のデータを格納することができるデータ構造で、格納した __データを取り出す__ 際に、__最後に入れたデータから順番に取り出す__ ことができるという性質があります。`stack` の返り値として、呼び出した関数が __新しい順で要素に代入された list が返される__ のはそのためです。
:::

言葉の説明では意味が非常にわかりづらいと思いますので、具体例を挙げます。下記のプログラムは、__`a` から `b`__ を、__`b` から `c` を呼び出す__ という __関数を定義__ しています。また、__最後の行の `a()`__ はどの関数からも呼び出されていません。従って `a` を呼び出すと __モジュール → `a` → `b` → `c` の順で関数が呼び出されます__。

```python
import inspect

def a():
    b()

def b():
    c()
    
def c():
    stack = inspect.stack()
    print(stack[0].function) # この関数の名前
    print(stack[1].function) # 1 つ前に呼び出した関数の名前
    print(stack[2].function) # 2 つ前に呼び出した関数の名前
    print(stack[3].function) # 3 つ前に呼び出した関数の名前
    
a()
```

実行結果

```
c
b 
a
<module>
```

__`c` のブロックの中__ で __`inspect.stack()`__ を実行すると、__`c` を実行するまでに呼び出した関数の情報__ が、__新しい順__ に __list の形式__ で格納されたデータが返り値として得られます。ただし、__最初の 0 番の要素__ には __`inspect.stack()` が記述された関数の情報__ が格納されるので、下記のような list が返されます[^3]。

__`[関数 c の情報, c を呼び出した関数 b の情報, b を呼び出した関数 a の情報, a を呼び出したモジュールの情報]`__

それぞれの要素に代入された関数の情報の __`function` 属性__ にはその __関数の名前が代入されます__。また、モジュールの場合は `function` 属性に `"<module>"` という文字列が代入されるので、上記の実行結果には `c`、`b`、`a`、`<module>` の順で関数の名前が表示されます。

上記から、関数を __直接呼び出した関数__ の情報は、__`inspect.stack()[1]`__ に代入されているので、__その名前を表示するプログラム__ は、下記のように記述すればよいことがわかります。

```python
inspect.stack()[1].function
```

:::note
上記のプログラムのコメントにも記しましたが、`inspect.stack()` の返り値の list の __`i` 番の要素__ には、__`i` 個前に呼び出した関数の情報__ が代入されると考えれば良いでしょう。ただし、`0` 個前の関数は、自分自身の関数を表します。
:::

下記は 5 行目で `stack` を利用して、__`draw_board`__ の __呼び出し元（caller）の関数（function）を表示__ するように修正したプログラムです。

```python
1  import inspect
2
3  def draw_board(self):
4      print("draw_board. move_count =", self.mb.move_count, 
5            "caller function =", inspect.stack()[1].function)
元と同じなので省略
6
7  Marubatsu_GUI.draw_board = draw_board
```

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

```python
import inspect

def draw_board(self):
    print("draw_board. move_count =", self.mb.move_count, 
          "caller function =", inspect.stack()[1].function)
    
    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)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]}　VS　{names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    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)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    self.update_widgets_status()   
    
Marubatsu_GUI.draw_board = draw_board
```
</details>

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

```diff_python
import inspect

def draw_board(self):
-   print("draw_board. move_count =", self.mb.move_count) 
+   print("draw_board. move_count =", self.mb.move_count, 
+         "caller function =", inspect.stack()[1].function)
元と同じなので省略

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

ゲーム盤の画像は省略しますが、上記の修正後に下記のプログラムを実行すると、実行結果のように、`draw_board` が __`play_loop` から呼び出された__ ことが確認できます。

```python
gui_play(ai=[None, ai1s])
```

実行結果

```
draw_board. move_count = 0 caller function = play_loop
```

次に、(1, 1) のマスをクリックして着手を行うと、下記のような表示が追加されます。

```
draw_board. move_count = 1 caller function = on_mouse_down
draw_board. move_count = 1 caller function = play_loop
draw_board. move_count = 1 caller function = change_step
draw_board. move_count = 2 caller function = play_loop
```

表示から、__`move_count` が 1 の場合に__ 、`draw_board` が __`on_mouse_down`__、__`play_loop`__、__`change_step`__ から __3 回呼び出される__ ことがわかります。また、その後で、 `play_loop` から呼び出されている `draw_board` は、__`move_count` の値が 2 になっている__ ので、2 手目の __AI が行った着手__ に対して呼び出された __`draw_board`__ の処理であることが __推測できます__。

`move_count` が 1 の場合に `draw_board` が 3 回も呼び出されるのは無駄なのですが、__1 回目の着手ではバグは発生していない__[^3]ので、その検証は後回しにし、続けて (2, 2) のマスをクリックして __2 回目の着手を行う__ と、下記のような表示が追加されます。

```
draw_board. move_count = 3 caller function = on_mouse_down
draw_board. move_count = 2 caller function = change_step
draw_board. move_count = 2 caller function = play_loop
```

実行結果から、__`draw_board`__ が 先程と同様に __3 回呼び出されている__ ことがわかりますが、よく見ると、__先程とは異なり__、__`on_mouse_down`__、__`change_step`__、__`play_loop`__ の順で呼び出されていることがわかります。おそらく __この違いはバグの原因と何か関係がありそう__ です。

また、1 回目の `draw_board` の呼び出しでは、`move_count` が 3 になっているので問題はありませんが、__2 回目__ の `change_step` から呼び出された場合に __`move_count` が 2 に変化__ し、3 回目の `draw_board` でも __`move_count` が 2 のままになっています__。おそらくこれが __2 回目の着手__ を行った際のゲーム盤の表示で __手数が更新されないバグの原因__ でしょう。

さらに、1 回目の着手の場合は表示されていた __4 回目の `draw_board` の呼び出し__ が、__今回は行われていません__。先ほどの __4 回目の `draw_board` の呼び出し__ は、おそらく __AI の着手に対応したもの__ だったので、今回は __AI が着手を行っていない__ ことが推測できます。

## `draw_board` の呼び出し元の関数の処理の検証

`draw_board` が、__`on_mouse_down`__、__`play_loop`__、__`change_step`__ という __3 つの関数から呼び出されている__ ことが確認できたので、__それぞれが行う処理について検証する__ ことにします。

__`on_mouse_down`__ は、マウスのクリック（またはキー入力）で __着手が行われた際に呼び出される関数__ です。下記は、`on_mouse_down` の定義で、6 行目で `move` メソッドで着手を行った後に、__ゲーム盤の描画を更新するため__ に 7 行目で __`draw_board` が呼び出されています__。

```python
1  def on_mouse_down(event):
2      # Axes の上でマウスを押していた場合のみ処理を行う
3      if event.inaxes and self.mb.status == Marubatsu.PLAYING:
4          x = math.floor(event.xdata)
5          y = math.floor(event.ydata)
6          self.mb.move(x, y)                
7          self.draw_board()
8          # 次の手番の処理を行うメソッドを呼び出す
9          self.mb.play_loop(self)
```   

__`play_loop`__ は、次の手番の処理を行うメソッドで、下記のプログラムの 11 行目のように、__手番の処理を行う際__ に、__最初に `draw_board` を呼び出して__ ゲーム盤の描画を更新します[^4]。

```python
 1  def play_loop(self, mb_gui):
略
 2      # ゲームの決着がついていない間繰り返す
 3      while self.status == Marubatsu.PLAYING:
 4          # 現在の手番を表す ai のインデックスを計算する
 5          index = 0 if self.turn == Marubatsu.CIRCLE else 1
 6          # ゲーム盤の表示
 7          if verbose:
 8              if gui:
 9                  # AI どうしの対戦の場合は画面を描画しない
10                  if ai[0] is None or ai[1] is None:
11                      mb_gui.draw_board()
12          # この後で手番の処理が行われる
略
```

__`change_step`__ は、__手数を変更する関数__ で、下記のプログラムのように、手数を変更した後で、`draw_board` を呼び出してゲーム盤の描画を更新します。

```python
def change_step(step):
    self.mb.change_step(step)
    # 描画を更新する
    self.draw_board() 
```

また、`change_step` は下記の状況になった場合に呼び出されます。

* リプレイボタンと待ったボタンをクリックした場合
* IntSlider の値が変化した場合

今回の操作では、リプレイボタンなどの操作は行っていないので、__IntSlider の `value` 属性の値が変化したこと__ で `change_step` が呼び出されている可能性が高いことがわかります。

:::note
本記事では行いませんが、そのことを実際に確認したい人は、`change_step` 内に、`print(inspect.stack()[1].function)` を記述して下さい。
:::

__IntSlider__ の __`value` 属性の値を変更する処理__ は、下記のプログラムのように、__`update_widgets_status`__ の中で行われます。また、`update_widgets_status` は __`draw_board` メソッド内から呼び出される__ ので、__`draw_board`__ メソッドを呼び出すことで、__IntSlider__ の __`value` 属性の値が変更される__ ことがわかります。

```python
def update_widgets_status(self):
略
    self.slider.value = self.mb.move_count
    self.slider.max = len(self.mb.records) - 1
```    

そこで、この後の処理の検証で __IntSlider の属性の値__ が上記で __どのように変更されたかがわかる__ ように、下記のプログラムの 2、5 行目のように、`update_widgets_status` 内で IntSlider の属性の値がどのように変更されたかを表示するようにします。

```python
1  def update_widgets_status(self):
元と同じなので省略
2      print(f"before: value = {self.slider.value} max = {self.slider.max}")
3      self.slider.value = self.mb.move_count
4      self.slider.max = len(self.mb.records) - 1
5      print(f"after:  value = {self.slider.value} max = {self.slider.max}")
6
7  Marubatsu_GUI.update_widgets_status = update_widgets_status
```

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

```python
def update_widgets_status(self):
    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)    
    print(f"before: value = {self.slider.value} max = {self.slider.max}")
    self.slider.value = self.mb.move_count
    self.slider.max = len(self.mb.records) - 1
    print(f"after:  value = {self.slider.value} max = {self.slider.max}")

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

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

```diff_python
def update_widgets_status(self):
元と同じなので省略
+   print(f"before: value = {self.slider.value} max = {self.slider.max}")
    self.slider.value = self.mb.move_count
    self.slider.max = len(self.mb.records) - 1
+   print(f"after:  value = {self.slider.value} max = {self.slider.max}")

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

## 2 回の着手を行った際の処理の検証

次に、バグの原因を調べるために、2 回の着手を行った際の処理を検証することにします。

### ゲーム開始時の処理の検証

ゲーム盤の画像は省略しますが、上記の修正後に下記のプログラムを実行すると、実行結果のように、`draw_board` が `play_loop` から呼び出され、IntSlider の `max` 属性が 0 に変化したことが確認できます。これは正常な処理です。

```python
gui_play(ai=[None, ai1s])
```

実行結果

```
draw_board. move_count = 0 caller function = play_loop
before: value = 0 max = 100
after:  value = 0 max = 0
```

### 1 回目の着手を行った後の最初の `draw_board` の検証

次に、(1, 1) のマスをクリックすると、下記のような表示が追加されます。

```
 1  draw_board. move_count = 1 caller function = on_mouse_down
 2  before: value = 0 max = 0
 3  after:  value = 0 max = 1
 4  draw_board. move_count = 1 caller function = play_loop
 5  before: value = 0 max = 1
 6  draw_board. move_count = 1 caller function = change_step
 7  before: value = 1 max = 1
 8  after:  value = 1 max = 1
 9  after:  value = 1 max = 1
10  draw_board. move_count = 2 caller function = play_loop
11  before: value = 1 max = 1
12  after:  value = 1 max = 2
```

上記の 1 ~ 3 行目から、__1 回目の `draw_board`__ は、1 回目の着手を行うためにゲーム盤の上でマウスを押したことによって呼び出された __`on_mouse_down` から呼び出された__ ことがわかります。また、2、3 行目から、その際に __IntSlider__ の __`value`__ 属性は __0 のまま変化しない__ が、__`max`__ 属性は __0 から 1 に変化する__ ことがわかりますが、これは __おかしくないでしょうか__？

下記の `update_widgets_status` で行われる処理では、IntSlider の __`value` 属性に `move_count` の値が代入__ されます。上記の 1 行目のメッセージから __`move_count`__ には __1 が代入されている__ ので、__`value`__ 属性には __1 が代入されるはず__ なのですが、実際には上記の 3 行目のメッセージから、__`value` 属性の値は 0 のまま変化していません__。

```python
def update_widgets_status(self):
略
    self.slider.value = self.mb.move_count
    self.slider.max = len(self.mb.records) - 1
```    

このような一見すると不可思議な処理が行われる理由は、IntSlider の __`max` 属性__ にあります。__`max` 属__ 性は、IntSlider の __`value` 属性の最大値__ を表す属性なので、`value` 属性に、__`max` 属性よりも大きな値を代入__ しようとすると、__自動的に `max` 属性の値に修正__ されます[^5]。

具体例を挙げます。下記のプログラムは、__`max` 属性に 10 を設定__ した IntSlider を作成し、その __`value` 属性に 20 を代入__ するプログラムですが、実行結果からわかるように、__`slider.value` 属性の値__ は、__`max` 属性の値__ に __自動的に修正__ されます。

```python
import ipywidgets as widgets

slider = widgets.IntSlider(max=10)
slider.value = 20
print(slider.value)
```

実行結果

```
10
```

:::note warn
今回の記事では説明しませんが、クラスには __インスタンスの属性に値を代入した際__ に、上記のように __属性の値を自動的に修正__ することができる __プロパティという仕組み__ があります。興味がある方は下記のリンク先を参照して下さい。

このように、__オブジェクトの属性__ に値を代入した際に、__代入しようとした値と異なる値が代入される場合がある__ 点に注意が必要です。また、そのようなことが起きるかどうかを知るには、利用するオブジェクトのクラスを定義する __モジュールのマニュアル（説明書）などを読む__ 必要があります。
:::

https://docs.python.org/ja/3/library/functions.html#property

先程の 1 回目の __`draw_board`__ は、__1 手目の着手が行われた後__ で呼び出されるので、__`move_count`__ と __`len(self.mb.records) - 1`__ の値は __1__ になっています。一方、__IntSlider__ はゲーム開始時に `value` と `max` 属性に `0` が代入された後は __更新されていません__。従って、`update_widgets_status` によって、以下のように `value` と `max` 属性の値が変化します。

|  | `value` | `max` |
|:--|:-:|:-:|
| 処理を行う前  | `0`  | `0`  |
| `self.slider.value = 1`  | `0`  | `0`  |
| `self.slider.max = 1`  | `0`  | `1`  |

上記で問題となるのは、__`self.slider.value = 1` を実行しても__、`max` 属性の値が `0` であるため、__`value` 属性の値が `0` のまま変化しない__ ことです。この問題は、下記のプログラムのように、__`max` 属性の値の変更を `value` 属性よりも前に記述する__ ことで、`max` 属性の値に 1 を代入した後に、`value` 属性に 1 が代入されるようになるため解決できます。

```python
def update_widgets_status(self):
略
    self.slider.max = len(self.mb.records) - 1
    self.slider.value = self.mb.move_count
```    

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

```diff_python
def update_widgets_status(self):
略
-   self.slider.value = self.mb.move_count
+   self.slider.max = len(self.mb.records) - 1
-   self.slider.max = len(self.mb.records) - 1
+   self.slider.value = self.mb.move_count
```
</details>

このバグは、__`max` 属性と `value` 属性の関係を正しく理解__ していないと、__原因をみつけて修正することが困難__ なバグです。また、そのことを理解していたとしても、__うっかり間違えてしまう可能性が高い__ バグで、実際に筆者も完全にうっかり間違えてしまいました。

実は、__上記の修正を行うことで、バグを修正できます__ が、今すぐに修正してしまうと、この後で __どのような処理が行われていたためバグが発生していたかがわからなくなる__ ので、バグを修正せずに、__このまま検証を続ける__ ことにします。

### 1 回目の着手を行った後の 2 回目の `draw_board` の検証

下記は、1 回目の着手を行った際の表示の再掲です

```
 1  draw_board. move_count = 1 caller function = on_mouse_down
 2  before: value = 0 max = 0
 3  after:  value = 0 max = 1
 4  draw_board. move_count = 1 caller function = play_loop
 5  before: value = 0 max = 1
 6  draw_board. move_count = 1 caller function = change_step
 7  before: value = 1 max = 1
 8  after:  value = 1 max = 1
 9  after:  value = 1 max = 1
10  draw_board. move_count = 2 caller function = play_loop
11  before: value = 1 max = 1
12  after:  value = 1 max = 2
```

上記の 4 行目から、__2 回目の `draw_board`__ の呼び出しは、__`play_loop` の中から行われている__ ことがわかりますが、その `play_loop` の呼び出しは、下記のプログラムの 9 行目のように、__`on_mouse_down`__ の 7 行目で __`draw_board` を呼び出した後__ で行われています。

```python
1  def on_mouse_down(event):
2      # Axes の上でマウスを押していた場合のみ処理を行う
3      if event.inaxes and self.mb.status == Marubatsu.PLAYING:
4          x = math.floor(event.xdata)
5          y = math.floor(event.ydata)
6          self.mb.move(x, y)                
7          self.draw_board()
8          # 次の手番の処理を行うメソッドを呼び出す
9          self.mb.play_loop(self)
```   

このことから、__`on_mouse_down`__ によって人間が着手を行った場合は、__`on_mouse_down`__ と __`play_loop`__ の中でそれぞれ 1 回ずつ、__合計 2 回 `draw_board` が呼びだされる__ ことがわかります。これは __本来は無駄な処理__ なのですが、この __無駄な処理が行われた結果__ 、__IntSlider__ の __`value` 属性の値__ が、下記のように __正しい値になります__。

下記は、2 回目の __`draw_board`__ で IntSlider に対して行われる処理です。__1 回目__ の __`draw_board`__ の処理によって、__`value` 属性と `max` 属性の値が 0 と 1 になっている__ ので、__今度__ は、__`self.slider.value = 1`__ によって、__`value` 属性の値が 1 になります__。

|  | `value` | `max` |
|:--|:-:|:-:|
| 処理を行う前  | `0`  | `1`  |
| `self.slider.value = 1`  | `1`  | `1`  |
| `self.slider.max = 1`  | `1`  | `1`  |

これが __1 回目の着手__ を行った際に、2 回目の着手を行った際のような __バグが発生しなかった原因__ です。ただし、バグが発生しなかったのは、__本来は 2 回行う必要のない__ `draw_board` の処理が __たまたま 2 回行われていた__ ためで、__バグの原因が修正されたわけではありません__。また、この後で検証しますが、実際に __2 回目の着手を行った際にはバグが発生する__ ので、1 回目の着手でバグが発生しなかったのは、__単なる偶然にすぎません__。

また、このように、実際には __バグが存在するにも関わらず__、__特定の条件が満たされない__ 場合はその __バグが表に出てこない__ ことがあります。そのようなバグは、一般的に __みつけづらく__、見つけるためには __プログラムの詳細な処理の検証が必要になる__ 場合があります。

### 1 回目の着手を行った後の 3 回目の `draw_board` の検証

ところで、下記の 2 回目の `draw_board` で表示されるメッセージがおかしいと思った人はいないでしょうか？下記では、2 行目で __before が表示された後__ で、__after が表示される前__ に __次の `draw_board` の表示__ が行われています。

これは、__`observe`__ によってウィジェットに結びつけられた __イベントハンドラ__ が、__指定した属性の値が変化__ した時に __即座に実行される__ という仕組みになっているためです。

```
略
1  draw_board. move_count = 1 caller function = play_loop
2  before: value = 0 max = 1
3  draw_board. move_count = 1 caller function = change_step
4  before: value = 1 max = 1
5  after:  value = 1 max = 1
6  after:  value = 1 max = 1
略
```

上記の 2 行目のメッセージの表示の後では、下記の手順で処理が行われます。

1. __`self.slider.value = 1`__ によって __`value` 属性の値が 0 から 1 に変更される__
2. その次の `self.slider.max = 1` が __実行される前__ に、`observe` によって IntSlider に結びつけられた、__`value` 属性の値が変更された際に呼び出される__ イベントハンドラである __`on_slider_changed`__ が __即座に呼び出される__
3. `on_slider_changed` は、下記のプログラムのように `change_step` を呼び出すので、その中から `draw_board` が呼び出され、3 ~ 5 行目のメッセージが表示される
4. `on_slider_changed` の処理が終了し、__`play_loop`__ から呼び出された __`draw_board` の処理が再開__ され、__6 行目__ で、__2 行目の before に対応する after の表示__ が行われる

```python
def on_slider_changed(changed):
    change_step(changed["new"])
```

下記のメッセージがどの関数から呼び出された `draw_board` であるかを右に示します。

```
略
1  draw_board. move_count = 1 caller function = play_loop      # play_loop
2  before: value = 0 max = 1                                   # play_loop
3  draw_board. move_count = 1 caller function = change_step    # change_step
4  before: value = 1 max = 1                                   # change_step
5  after:  value = 1 max = 1                                   # change_step
6  after:  value = 1 max = 1                                   # play_loop
略
```

:::note
3 ~ 5 行目のメッセージは、__`change_step` から呼び出された `draw_board`__ で行われた処理です。__2 行目の処理の後__ で、__`value` 属性の値が `1` に変化する__ ので、4、5 行目のメッセージからわかるように、`change_step` から呼び出された `draw_board` では、__IntSlider の `value` 属性の値は変化しません__。そのため、4 行目と 5 行目の間で、新たに `on_slider_changed` の処理が呼び出されることはありません。

また、2、6 行目のメッセージから、__`play_loop`__ から呼び出された `draw_board` で、IntSlider の __`value` 属性の値が 0 から 1 に変化したことがわかります__。
:::

:::note 
下記の話は細かい話なので、意味が分からない場合は読み飛ばしてください。

[以前の記事](https://qiita.com/ysgeso/items/1920c68f5a1e8a998796#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E9%A7%86%E5%8B%95%E5%9E%8B%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF)で、イベントハンドラは、イベントループから JupyterLab のセルなどのや、イベントハンドラの __プログラムの処理が完了した後で呼び出される__ と説明しましたが、__`observe`__ によって結び付けられた、ウィジェットの属性が変化すると呼び出されるイベントハンドラは、別の仕組みで __属性の値が変化した時点__ で __即座に呼び出される__ ようです。これは筆者も勘違いていましたので注意して下さい。

なお、`on_button_clicked` などの、ボタンやキーの操作に対するイベントハンドラは、以前の記事で説明した通り、イベントループから呼び出されるので、プログラムの処理が完了した後で呼び出されます。
:::

### 1 回目の着手を行った後の 4 回目の `draw_board` の検証

下記は、4 回目の `draw_board` が表示するメッセージです。

```
略
draw_board. move_count = 2 caller function = play_loop
before: value = 1 max = 1
after:  value = 1 max = 2
```

__`move_count` が 2__ になっており、__`play_loop` から呼び出されている__ ので、これは __AI が 2 手目の着手を行った後__ で `play_loop` から呼び出されたものです。また、__1 回目の `draw_board` と同じ理由__ で、IntSlider の __`value`__ 属性が __1 から 2 に変化しない__ という __バグが発生している__ ことがわかります。このバグは、画面の表示に現れていないと筆者は最初は思っていたのですが、よく見ると __1 手目の着手を行った後__ で、AI が着手を行い __2 手目の局面になっている__ にも関わらず、下図のように、現在の手数を表す __IntSlider の値が 2 ではなく、1 になっている__ という点で、__バグの存在が画面に反映されています__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/dd89969b-d8fd-4f58-e73b-270dfc620d91.png" width="250">

このことから、__1 手目の着手を行った後は__ 、一見すると __バグが発生していないように見えていました__ が、__実際にはバグが発生していた__ ことがわかります。このような、一見しただけではバグが発生していることがわからないようなバグは、見つけることはかなり困難です。

### 2 回目の着手を行った後の最初の `draw_board` の検証

次に、(2, 2) のマスをクリックすると、下記のような表示が追加されます。

```
1  draw_board. move_count = 3 caller function = on_mouse_down
2  before: value = 1 max = 2
3  draw_board. move_count = 2 caller function = change_step
4  before: value = 2 max = 2
5  after:  value = 2 max = 3
6  after:  value = 2 max = 3
7  draw_board. move_count = 2 caller function = play_loop
8  before: value = 2 max = 3
9  after:  value = 2 max = 3
```

__自分の 2 回目__ の着手では、__3 手目__ の着手が行われるので、__`move_count` には 3 が代入__ されますが、IntSlider の __`max` 属性には 2 が代入__ されているので、__`self.slider.value = 3`__ を実行しても __`value` 属性__ には `max` 属性の値である __2 が代入__ されます。

ここまでは、1 回目の着手の場合と同じですが、上記の 2 行目からわかるように、__`value` 属性の元の値__ は先ほど説明したように、直前の 2 手目で AI の着手が行われても 2 にはならず、__1 のまま__ です。そのため、__`self.slider.value = 3`__ によって、__`value` 属性の値__ は 1 から 2 に __変化する__ ことになるため、__1 回目の着手の場合と異なり__ `on_slider_changed` が呼び出され、その中から `change_step(changed["new"])` が呼び出されることになります。

### 2 回目の着手を行った後の 2 回目の `draw_board` の検証

下記は、2 回目の着手を行った後の表示の右に、どの関数から `draw_board` が呼び出されたかを記載したものです。

```
1  draw_board. move_count = 3 caller function = on_mouse_down   # on_mouse_down
2  before: value = 1 max = 2                                    # on_mouse_down
3  draw_board. move_count = 2 caller function = change_step     # change_step
4  before: value = 2 max = 2                                    # change_step
5  after:  value = 2 max = 3                                    # change_step
6  after:  value = 2 max = 3                                    # on_mouse_down
略
```

`change_step` から呼び出される __2 回目の `draw_board`__ では、上記の 3 行目から __`move_count` に 2 が代入__ されていることがわかります。このようなことが起きる原因は以下の通りです。

* 1  回目の `draw_board` の中で、IntSlider の `value` 属性の値が 1 から 2 に変化する
* `on_slider_changed` が呼び出され、`change_step(changed["new"])` が呼び出される
* `changed["new"]` には、IntSlider の変化後の値である 2 が代入されているので __2 手目の局面に移動する処理__ が行われる
* __2 手目の局面に移動した後__ で、`change_step` の中から `draw_board` が呼び出されるので、__`move_count` には 2 が代入__ されている。また、表示される局面も、2 手目の局面になる

バグの原因をまとめると、以下のようになります。

* __直前の AI の着手__ によって IntSlider の __`value` 属性の値__ が 2 になるはずだったが、`max` 属性の値のせいで __1 になってしまった__
* 本当は、`on_mouse_down` から呼び出された `draw_board` の処理で、IntSlider の __`value` 属性の値を 3 にするはず__ だったが、`max` 属性の値のせいで __2 になってしまった__
* __`value`__ 属性が __1 から 2 に変化した__ ので、`on_slider_changed` が呼ばれた
* `on_slider_changed` の中で、__`change_step`__ によって __3 手目の局面に移動するはずだった__ が、IntSlider の __`value`__ 属性の値が __バグで 2 になってしまったせい__ で、__2 手目の局面に移動してしまった__

また、1 回目の着手でこのバグが発生しなかった理由は、以下の通りです。

* 1 回目の着手では、__直前の AI の着手が行われていない__ ため、__`value`__ 属性の値が __正しい 0 の値__ であった
* 本当は、`on_mouse_down` から呼び出された `draw_board` の処理で、IntSlider の __`value` 属性の値を 1 にするはず__ だったが、`max` 属性の値のせいで __0 になってしまった__
* __元の `value` 属性__ の値が __バグのせいで 0 だった__ ので、上記の処理によって __偶然 `value` 属性が変化しなかった__ ため `on_slider_changed` は呼び出されず、そのことによるバグは発生しなかった

上記からわかるように、1 回目の着手で __バグが表面に現れなかった理由__ は、__たまたま__ 直前の手番で AI の着手が行われなかっため `value` 属性の値が変化しなかったからに __過ぎません__。

### 2 回目の着手を行った後の 3 回目の `draw_board` の検証

下記は、3 回目の `draw_board` による表示です。下記の 1 行目から、3 回目の `draw_board` は `move_count` が 2 の状態で `play_loop` から呼ばれますが、__`move_count` が 2 の場合__ は、__人間が担当__ する 〇 の手番なので、__`play_loop`__ では __AI の着手は行われません__。そのため、`draw_board` では、__2 手目の着手が行われた後の局面が描画__ されます。

```
略
1  draw_board. move_count = 2 caller function = play_loop
2  before: value = 2 max = 3
3  after:  value = 2 max = 3
```

以上が、プログラムの処理の検証による、バグの原因の解明の手順でした。

ちなみに先程説明した、__IntSlider の `max` 属性に値を代入する前に `value` 属性に値を代入する__ というバグは、そのようなバグの例を示すためにわざと発生させたものではなく、完全に筆者のうっかりミスです。ただし、このようなうっかりミスは __慣れていても完全に避けることは難しい__ と思いますので、バグの発見の手順も含めて詳しく解説しました。

:::note
実際にはバグを見つけるまでに、いくつか別の場所に `print` で変数の値などを表示する作業を行いましたが、それらをすべて記述すると長くなるので省略しました。
:::

## バグの修正

バグの修正は、先程説明したように `update_widgets_status` 内で、下記のプログラムの 3、4 行目のように、__`value` 属性より先に `max` 属性の値を変更する__ ことで行うことができます。

```python
1  def update_widgets_status(self):
元と同じなので省略
2      print(f"before: value = {self.slider.value} max = {self.slider.max}")
3      self.slider.max = len(self.mb.records) - 1
4      self.slider.value = self.mb.move_count
5      print(f"after:  value = {self.slider.value} max = {self.slider.max}")
6
7  Marubatsu_GUI.update_widgets_status = update_widgets_status
```

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

```python
def update_widgets_status(self):
    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)    
    print(f"before: value = {self.slider.value} max = {self.slider.max}")
    self.slider.max = len(self.mb.records) - 1
    self.slider.value = self.mb.move_count
    print(f"after:  value = {self.slider.value} max = {self.slider.max}")

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

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

```diff_python
def update_widgets_status(self):
元と同じなので省略
    print(f"before: value = {self.slider.value} max = {self.slider.max}")
-   self.slider.value = self.mb.move_count
+   self.slider.max = len(self.mb.records) - 1
-   self.slider.max = len(self.mb.records) - 1
+   self.slider.value = self.mb.move_count
    print(f"after:  value = {self.slider.value} max = {self.slider.max}")

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

上記の修正後に、下記のプログラムを実行し、(1, 1) に着手を行うと実行結果のような表示が行われ、メッセージから、__`draw_board`__ で、__IntSlider__ の __`value` 属性の値__ が __`move_count` の値に正しく変化し__、図からも __IntSlider の値が正しく 2 になる__ ことが確認できます。 

```python
gui_play(ai=[None, ai1s])
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7706fd97-b1c7-251c-ef66-148439ffffac.png" width="250">

```python
draw_board. move_count = 1 caller function = on_mouse_down
before: value = 0 max = 0
draw_board. move_count = 1 caller function = change_step
before: value = 1 max = 1
after:  value = 1 max = 1
after:  value = 1 max = 1
draw_board. move_count = 1 caller function = play_loop
before: value = 1 max = 1
after:  value = 1 max = 1
draw_board. move_count = 2 caller function = play_loop
before: value = 1 max = 1
draw_board. move_count = 2 caller function = change_step
before: value = 2 max = 2
after:  value = 2 max = 2
after:  value = 2 max = 2
```

続けて (2, 2) に 2 回目の着手を行うと、図のように 4 手目の局面が表示され、__バグが修正されたことが確認できます__。なお、メッセージは 1 回目の着手と同様なので省略します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3ad6355d-bd87-894d-8349-a429cdd7bac3.png" width="250">

`value` 属性と `max` 属性への値の __代入の順序の間違いだけ__ で、これほど見つけづらく、わかりづらいバグが発生することに驚いた人がいるかもしれませんが、そのような __一見すると些細な違いから__ このような __見つけづらいバグが発生__ することは　__実際に良くあります__。このようなバグの原因を見つけて修正するためには、__豊富なデバッグ作業の経験__ と、今回の記事で行ったような __地道なデバッグの作業がどうしても必要になります__ ので参考にして下さい。

## 無駄な処理の削除

先程のメッセージから下記の手順で処理が行われていることがわかります。

1. `on_mouse_down` が呼び出されて着手が行われ、`draw_board` が呼び出される
2. `draw_board` で 1 手目の局面が描画され、IntSlider の `value`、`max` 属性が 0 から 1 になる
3. `value` 属性の値が変更されたので、`on_slider_changed` が呼び出され、その中で `change_step(1)` が呼び出される
4. `change_step` の中で 1 手目の局面に移動する処理と `draw_board` が呼び出される
5. `draw_board` で 1 手目の局面が描画される。IntSlider の属性の値は変化しない
6. `on_mouse_down` から `play_loop` が呼び出され、その中で `draw_board` が呼び出される
7. `draw_board` で 1 手目の局面が描画される。IntSlider の属性の値は変化しない
8. AI の手番なので `play_loop` の中で AI の着手が行われ、`draw_board` が呼び出される
9. `draw_board` で 2 手目の局面が描画され、IntSlider の `value`、`max` 属性が 1 から 2 になる
10. `value` 属性の値が変更されたので、`on_slider_changed` が呼び出され、その中で `change_step(2)` が呼び出される
11. `change_step` の中で 2 手目の局面に移動する処理と `draw_board` が呼び出される
12. `draw_board` で 2 手目の局面が描画される。IntSlider の属性の値は変化しない

下記はメッセージがどの関数から呼び出されたかを右に記したものです。

```python
draw_board. move_count = 1 caller function = on_mouse_down  # on_mouse_down
before: value = 0 max = 0                                   # on_mouse_down
draw_board. move_count = 1 caller function = change_step    # change_step（1 回目）
before: value = 1 max = 1                                   # change_step（1 回目）
after:  value = 1 max = 1                                   # change_step（1 回目）
after:  value = 1 max = 1                                   # on_mouse_down
draw_board. move_count = 1 caller function = play_loop      # play_loop（1 回目）
before: value = 1 max = 1                                   # play_loop（1 回目）
after:  value = 1 max = 1                                   # play_loop（1 回目）
draw_board. move_count = 2 caller function = play_loop      # play_loop（2 回目）
before: value = 1 max = 1                                   # play_loop（2 回目）
draw_board. move_count = 2 caller function = change_step    # change_step（2 回目）
before: value = 2 max = 2                                   # change_step（2 回目）
after:  value = 2 max = 2                                   # change_step（2 回目）
after:  value = 2 max = 2                                   # play_loop（2 回目）
```

2 回目の着手を行った場合に行われる処理は同様なので省略します。興味がある方はクリックして表示されるメッセージを確認して下さい。

上記の処理で、__無駄な処理が行われている__ と思った人はいないでしょうか？具体的にどの処理が無駄であるかについて少し考えてみて下さい。

上記の __手順 4__ では、`change_step` の中で __1 手目の局面に移動する処理__ が行われていますが、__手順 1__ によって、現在の局面は __既に 1 手目の局面になっています__。従って、__手順 4__ の処理は、既に 1 手目の局面であるにも関わらず、`change_step` で 1 手目の局面に移動するという __無駄な処理__ が行われています。また、1 手目の局面は __手順 2 で描画済__ なので、__改めて描画し直す必要はありません__。これは AI が着手を行った場合の __手順 11 でも同様__ です。

__`change_step`__ で局面を __移動する必要がある__ のは __現在の手数と異なる手数に移動する場合__ です。__手順 4__ は、__`on_slider_changed` から呼び出されている__ ので、__必要な場合だけ `change_step` を呼び出す__ ように `on_slider_changed` を修正することで、__無駄な `change_step` と `draw_board` を呼び出さないようにする__ ことができます。

もう一つの無駄は、上記の手順 2、5、7 と 9、12 です。それぞれ 1 手目と 2 手目の局面を __`draw_board`__ で __表示する処理が複数回行われています__ が、この処理は __1 回だけ行えば十分__ です。処理 5 と 12 は `change_step` から呼ばれる処理で、上記の `on_slider_changed` の修正を行うことで行われなくなります。そこで、残りの `on_mouse_down` 内で `draw_board` を呼び出す __処理 2 を行わないようにする__ ことにします。

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

* __3、4 行目__：`move_count` と、変化した IntSlider の `value` 属性の値が異なる場合だけ、`change_step` を呼び出すように修正する
* 12 行目の下にあった、`draw_board` を呼び出す処理を削除する

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_slider_changed(changed):
 3          if self.mb.move_count != changed["new"]:
 4              change_step(changed["new"])
元と同じなので省略
 5      # ゲーム盤の上でマウスを押した場合のイベントハンドラ
 6      def on_mouse_down(event):
 7          # Axes の上でマウスを押していた場合のみ処理を行う
 8          if event.inaxes and self.mb.status == Marubatsu.PLAYING:
 9              x = math.floor(event.xdata)
10              y = math.floor(event.ydata)
11              self.mb.move(x, y)                
12              # この下にあった draw_board を呼び出す処理を削除する
13              # 次の手番の処理を行うメソッドを呼び出す
14              self.mb.play_loop(self)
元と同じなので省略
15   
16  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

```python
def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        self.mb.restart()
        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.draw_board()
        
    # イベントハンドラをボタンに結びつける
    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.draw_board()        

    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)
            self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    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,            
        }
        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_slider_changed(changed):
-       change_step(changed["new"])
+       if self.mb.move_count != changed["new"]:
+           change_step(changed["new"])
元と同じなので省略
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    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)
            self.mb.move(x, y)                
-           self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

上記の修正後に、下記のプログラムを実行し、(1, 1) に着手を行うと、実行結果のように、`draw_board`  が `play_loop` のみから呼び出されるようになり、__無駄な `draw_board` の呼び出しが行われなくなった__ ことが確認できます。

```python
gui_play(ai=[None, ai1s])
```

実行結果

```python
draw_board. move_count = 1 caller function = play_loop
before: value = 0 max = 0
after:  value = 1 max = 1
draw_board. move_count = 2 caller function = play_loop
before: value = 1 max = 1
after:  value = 2 max = 2
```

以上でキー操作による GUI の処理の実装は完了です。

なお、`draw_board` などに記述したデバッグのメッセージはもう必要がなくなったので、marubatsu.py のほうに今回のプログラムを反映する際は削除することにします。

# リプレイ中の着手の禁止について

これで、筆者が思いついたリプレイ機能に関連する実装は完了です。

なお、リプレイ中に別の着手を行うことができる点に違和感を感じている人がいるかもしれません。そのような場合は、`cretate_event_handler` 内の `on_mouse_down` を下記のプログラムのように修正して下さい。なお、本記事では下記のプログラムは採用しません。

* __4 行目__：最後の着手が行われた局面であることを条件式に加える

```python
 1  # ゲーム盤の上でマウスを押した場合のイベントハンドラ
 2  def on_mouse_down(event):
 3      # Axes の上でマウスを押していた場合で、最後の着手が行われた局面でのみ処理を行う
 4      if event.inaxes and self.mb.status == Marubatsu.PLAYING and self.mb.move_count == len(self.mb.records) - 1:
 5          x = math.floor(event.xdata)
 6          y = math.floor(event.ydata)
 7          self.mb.move(x, y)                
 8          self.draw_board()
 9          # 次の手番の処理を行うメソッドを呼び出す
10          self.mb.play_loop(self)
```

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

```python
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合で、最後の着手が行われた局面でのみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING and self.mb.move_count == len(self.mb.records) - 1:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)
```
</details>

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

```diff_python
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合で、最後の着手が行われた局面でのみ処理を行う
-       if event.inaxes and self.mb.status == Marubatsu.PLAYING and self.mb.move_count == len(self.mb.records) - 1:
+       if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)
```
</details>

:::note
本記事では採用しませんが、次回の記事で説明する CheckBox を利用して、リプレイ中の着手を行えるかどうかを切り替えることができるようにすることもできます。

このように、__アイディア次第で〇×ゲームの GUI の機能をいくらでも拡張することができる__ ので、良いアイディアを思いついた方は実装してみて下さい。
:::

# 今回の記事のまとめ

今回の記事では、最初にキー入力による GUI の実装を完了しました。

また、思わぬバグが発生したので、そのバグの原因と修正方法について説明しました。

今回の記事で GUI の実装を完了する予定だったのですが、予定外のバグが発生し、その説明を行いましたので、GUI の実装の完了は次回に持ち越すことにします。

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

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

https://github.com/ysgeso/marubatsu/blob/master/085/marubatsu.ipynb

以下のリンクは、今回の記事で更新した marubatsu.py です。

https://github.com/ysgeso/marubatsu/blob/master/085/marubatsu_new.py

# 次回の記事

https://qiita.com/ysgeso/items/5e5b2fdaf9eda23a9230

[^1]:後で説明しますが、実際には __1 回目の着手でバグは発生__ しています。__一見するとバグが発生していないように見える__ のでこのように表記しました
[^2]:他にも関数に関する様々な情報が代入された属性があります。詳細は、こちらの[リンク先](https://docs.python.org/ja/3/library/inspect.html#inspect.FrameInfo)を参照して下さい
[^3]:実際には 4 番以降の要素にも値が代入されますが、今回の説明では必要がない要素なので説明は省略します
[^4]:AI どうしの対戦の場合は `draw_board` を呼び出さないという工夫を行っていますが、先程の例の場合は AI どうしの対戦ではないので、手番の処理を行う前に `draw_board` が呼び出されます
[^5]:`min` 属性も同様です