Pythonで〇×ゲームのAIを一から作成する　その72　リセットボタンによるゲームのリセット

# 目次と前回の記事

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

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

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

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

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

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

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

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

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

# ボタンによるゲームのリセット

前回の記事で、__〇×ゲーム__ を __GUI で遊べる__ ようになりました。今回の記事では、__ipywidgets__ を利用して、__ゲーム__ を __リセットする__ ための __ボタン__ を __実装__ することにします。

## リセットボタンの必要性

__これまで__ の記事では、__新しいゲーム__ を __開始__ するたびに、__`play`__ メソッドを __実行__ してきましたが、__その際__ に __下記__ の __メッセージが表示__ されたことはないでしょうか。なお、__実際のメッセージ__ は __1 行__ で表示されますが、__わかりやすいよう__ に __下記__ では __改行__ を行っています。

```
C:\Users\ys\AppData\Local\Temp\ipykernel_1672\1311249836.py:6: RuntimeWarning: 
More than 20 figures have been opened. Figures created through the pyplot interface
(`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume
too much memory. (To control this warning, see the rcParam `figure.max_open_warning`).
Consider using `matplotlib.pyplot.close()`.
  plt.subplots(figsize=[0.1, 0.1])
```

__上記__ のメッセージの中の、__下記__ のメッセージの __意味__ は、__以下の通り__ です。

```
More than 20 figures have been opened.
Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are
retained until explicitly closed and may consume too much memory.
```

__20 より多く__（more than 20）の __画像__（figures）が __開かれている__（has been opened）。
__pyplot__ の（`pyplot.figure` メソッドなどの）__インターフェース__[^1]（interface）よって __作成__ された（created through）__Figure__ は、__明示的__ に（explicitly）__閉じられる__（closed）__まで__（until）__保持される__（retained）ため、__多く__（too many）の __メモリ__（memory）を __消費__（consume）する __可能性__（may）がある。

[以前の記事](https://qiita.com/ysgeso/items/1920c68f5a1e8a998796#pltshow-%E3%81%AE%E5%87%A6%E7%90%86%E3%81%AE%E5%A4%89%E5%8C%96)で説明したように、__`%matplotlib widget`__ を __実行__ した場合は、__作成__ された __Figure__ が、__pyplot が 管理__ する __Figure__ の一覧から __自動的__ に__削除__ されることは __ありません__。__`play`__ メソッドは、__その中__ で __`subplots`__ メソッドによって __新しい Figure__ を __作成する__ ため、__`play`__ メソッドを __実行するたび__ に __Figure__ が __1 つ作成__ されますが、__作成された Figure__ は、__すべて残り続ける__ ために __上記__ のような __警告のメッセージ__ が __表示__ されます。

__上記__ の __メッセージ__ を __自分の目__ で __確認したい__ 人は、__下記の__ プログラムを __実行__ してみて下さい。__下記__ のプログラムは、__`plt.subplots`__ によって、__21 個__ の __Figure__ を __作成__ するプログラムで、__実行__ すると __上記のメッセージ__ と、__21 個__ の __画像__ が __描画__ されます。なお、__描画__ される Figure の __画像__ を __小さくするため__ に、__`figsize=[0.1, 0.1]`__ を __実引数に記述__ しました。実行結果は長いので省略します。

```python
%matplotlib widget
import matplotlib.pyplot as plt
import japanize_matplotlib

for i in range(21):
    plt.subplots(figsize=[0.1, 0.1])
```

__プログラム__ の __バグ__ を __修正した際__ に、__動作を確認__ するために __`play`__ メソッドを __何度も実行__ することは __仕方がありません__ が、__〇×ゲーム__ の __新しいゲーム__ を __遊ぶため__ に __`play`__ メソッドを __毎回実行__ すると、__実行した回数__ だけ __Figure__ が __作成される__ ことになるため、その分だけコンピューターの __メモリ__ が __消費されてしまう__ ことになります。そこで、__ipywidgets__ の __ボタン__ の __クリック__ で __〇×ゲーム__ を __リセットできる__ ようにすることで、__`play`__ メソッドを __実行しなくても__、__新しいゲームを始める__ ことが __できるよう__ にします。


## ボタンの作成とイベントハンドラの登録のおさらい

__ipywidgets__ の __ボタン__ は、[以前の記事](https://qiita.com/ysgeso/items/1920c68f5a1e8a998796#%E3%83%9C%E3%82%BF%E3%83%B3%E3%81%AE%E3%82%A6%E3%82%A3%E3%82%B8%E3%82%A7%E3%83%83%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E3%81%A6%E9%85%8D%E7%BD%AE%E3%81%99%E3%82%8B)で説明した方法で、__下記__ のプログラムで作成し、__JupyterLab に表示__ することが __できます__。

```python
import ipywidgets as widgets

button = widgets.Button(description="Click me")
display(button)
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7644037a-84de-2b1b-4543-ffd9ad87a726.png" width="150">

__作成__ した __ボタン__ を __クリック__ した際に __実行__ する __イベントハンドラ__ は、[以前の記事](https://qiita.com/ysgeso/items/1920c68f5a1e8a998796#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%81%AE%E5%AE%9A%E7%BE%A9)で説明した方法で、__下記__ のプログラムのように __定義__ して、__ボタン__ に __結びつける__ ことが __できます__。実行結果は、上記のプログラムを実行した後で、ボタンを 3 回クリックした場合の図です。

```python
def on_button_clicked(b):
    print("ボタンがおされたよ！")

button.on_click(on_button_clicked)
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/ef3fc75d-a088-1a05-e70f-29b54070a599.png" width="150">

## 〇×ゲームのリセットボタンの表示

__〇×ゲーム__ の __リセットボタン__ は __`play`__ メソッドを __実行__ した際に __表示__ するので、__下記__ のプログラムのように、__上記の処理__ を、__`play`__ メソッドで __Figure__ を __作成する前__ に __記述__ します。

* __8 ~ 16 行目__：__`gui`__ が __`True`__ の場合に、__上記の処理__ を、__`play`__ メソッドの中に __記述__ する

```python
 1  from marubatsu import Marubatsu
 2  import math
 3
 4  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
 5      # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
 6      if gui:
 7          # リセットボタンを配置する
 8          button = widgets.Button(description="リセット")
 9          display(button)
10        
11          # リセットボタンのイベントハンドラを定義する
12          def on_button_clicked(b):
13              print("ボタンがおされたよ！")    
14              
15          # イベントハンドラをリセットボタンに結びつける
16          button.on_click(on_button_clicked)
17
18          fig, ax = plt.subplots(figsize=[size, size])
元と同じなので省略        
19
20  Marubatsu.play = play
```

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

```python
from marubatsu import Marubatsu
import math

def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # 〇×ゲームを再起動する
    self.restart()
   
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # リセットボタンを配置する
        button = widgets.Button(description="リセット")
        display(button)
        
        # リセットボタンのイベントハンドラを定義する
        def on_button_clicked(b):
            print("ボタンがおされたよ！")    
            
        # イベントハンドラをリセットボタンに結びつける
        button.on_click(on_button_clicked)

        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合で、ゲーム中の場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)
        
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
            

    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.draw_board(ax)
        else:
            print(self)
            
    return self.status       

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

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

```diff_python
from marubatsu import Marubatsu
import math

def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # リセットボタンを配置する
+       button = widgets.Button(description="リセット")
+       display(button)
        
        # リセットボタンのイベントハンドラを定義する
+       def on_button_clicked(b):
+           print("ボタンがおされたよ！")    
            
        # イベントハンドラをリセットボタンに結びつける
+       button.on_click(on_button_clicked)

        fig, ax = plt.subplots(figsize=[size, size])
元と同じなので省略        

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__実行結果__ のように、__リセットボタン__ が __ゲーム盤の上__ に __表示__ されるようになり、__リセットボタン__ を __クリックするたび__ に、__実行結果__ のように __メッセージ__ が __表示される__ ようになります。__実際__ に下記のプログラムを実行して __確認__ してみて下さい。なお、__下記__ は __人間どうし__ の __対戦__ です。

```python
mb = Marubatsu()
mb.play(ai=[None, None], gui=True)
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7c856dd9-8205-c5cc-d38d-6d655ce9ba1c.png" width="150">

:::note
実は、__ボタン__ に __関する処理__ を、__Figure__ を __作成する処理__ の __後に記述__ しても、__実行結果__ は __変化せず__、__ボタン__ は __ゲーム盤__ の __画像より上__ に __表示__ されます。__ウィジェット__ の __表示__ の __レイアウト__ を __変える方法__ については __今後の記事__ で __紹介__ します。
:::

## リセットボタンのクリックによるゲームのリセットの処理

__リセットボタン__ を __クリック__ した時の __処理__ は、__`Marubatsu`__ クラスの __`restart`__ メソッドによって __行う__ ことが __できます__。また、__上記__ のプログラムでは、__`on_button_clicked`__ の __イベントハンドラ__ を、__`play`__ メソッドの __ローカル関数__ として __定義__ したので、__`on_mouse_down`__ と __同様__ に、__`Marubatsu`__ クラスの __インスタンス__ が __代入__ された __`play`__ メソッドの __仮引数 `self`__ を __そのまま利用__ することが __できます__。従って、__`on_button_clicked`__ を __下記__ のように __修正__ する事で、__リセットボタン__ を __クリック__ すると __〇×ゲーム__ を __リセットできる__ ようになります。

* __4 行目__：__リセットボタン__ が __押された場合__ に呼び出される __`on_button_clicked`__ で、__`restart`__ メソッドを呼び出して __ゲーム__ を __リセットする__

```python
1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略  
2      # リセットボタンのイベントハンドラを定義する
3      def on_button_clicked(b):
4          self.restart()     
元と同じなので省略
5    
6  Marubatsu.play = play
```

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

```python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # 〇×ゲームを再起動する
    self.restart()
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()     
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)

                # 現在の手番を表す ai のインデックスを計算する
                index = 0 if self.turn == Marubatsu.CIRCLE else 1
                # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
                if self.status == Marubatsu.PLAYING and ai[index] is not None:                
                    x, y = ai[index](self, **params[index])
                    self.move(x, y) 
                    self.draw_board(ax)
                   
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.draw_board(ax)
        else:
            print(self)
            
    return self.status
    
Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略  
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
-       print("ボタンがおされたよ！") 
+       self.restart()     
元と同じなので省略
    
Marubatsu.play = play
```
</details>

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ し、__いくつか__ のマスに __着手__ を __行った後__ で、__リセットボタン__ を __クリック__ してみて下さい。

```python
mb.play(ai=[None, None], gui=True)
```

残念ながら __リセットボタン__ を __クリック__ しても __画面の表示__ は __変化しません__。しかし、__その後__ で __マスの上__ で __マウスを押す__ と、__押したマス以外__ の __マーク__ が __リセット__ されるという、__一見__ すると __不思議なこと__ がおきます。__下図右__ は、__下図左__ の __局面__ で __リセットボタン__ を __クリックした後__ で、__(1, 1)__ の __マスの上__ で __マウスを押した__ 場合です。何故このようなことが起きるかについて少し考えてみて下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e315f2a0-ba79-f059-548c-5240cc000dcd.png" width="150">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/8527617c-7011-af4f-793a-ee2318d96eb1.png" width="150">

## 問題の検証と修正

__上記の問題__ は、__`restart`__ メソッドによって __〇×ゲーム__ を __リセットした後__ で、__ゲーム盤の描画__ を __更新していない__ ことが __原因__ です。

__これまでのプログラム__ で __着手を行った後__ に __ゲーム盤の描画__ が __更新__ されていたのは、__下記__ のプログラムの __8 行目__ のように、__`move`__ メソッドで __着手__ を __行った後__ で、__`draw_board`__ メソッドを __実行__ して __ゲーム盤の画像__ を __更新する処理__ を行っていたからです。

```python
1  # ローカル関数としてイベントハンドラを定義する
2  def on_mouse_down(event):
3      # Axes の上でマウスを押していた場合で、ゲーム中の場合のみ処理を行う
4      if event.inaxes and self.status == Marubatsu.PLAYING:
5          x = math.floor(event.xdata)
6          y = math.floor(event.ydata)
7          self.move(x, y)                
8          self.draw_board(ax)
以下略
```

:::note warn
初心者の方でコンピュータは賢いはずだから、__ゲーム__ を __リセット__ したら __気を利かせて画面の描画__ を __更新してくれる__ だろうと __考える人__ が __いるかもしれません__ が、__プログラム__ は、__記述__ した事 __しか行わない__ 点に __注意__ して下さい。__ゲーム盤__ の __状況が変化__ した際に、このような __描画の更新__ の __し忘れ__ は __よくあるミス__ なので紹介しました。

:::

__下記__ のプログラムの __5 行目__ のように、__`restart`__ メソッドを __呼び出した後__ で、__`draw_board`__ メソッドを __呼び出す__ ように __修正__ することで、__この問題__ を __解決__ することが __できます__。

```python
1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2      # リセットボタンのイベントハンドラを定義する
3      def on_button_clicked(b):
4          self.restart()       
5          self.draw_board(ax)       
6
元と同じなので省略         
7    
8  Marubatsu.play = play
```

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

```python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # 〇×ゲームを再起動する
    self.restart()
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
        self.draw_board(ax)       
         
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)

                # 現在の手番を表す ai のインデックスを計算する
                index = 0 if self.turn == Marubatsu.CIRCLE else 1
                # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
                if self.status == Marubatsu.PLAYING and ai[index] is not None:                
                    x, y = ai[index](self, **params[index])
                    self.move(x, y) 
                    self.draw_board(ax)
                   
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.draw_board(ax)
        else:
            print(self)
            
    return self.status
    
Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
+       self.draw_board(ax)       

元と同じなので省略         
    
Marubatsu.play = play
```
</details>

下図は、__修正後__ の __`on_button_clicked`__ のフローチャートです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3cafc1ae-8648-f65d-374f-87d44b2c24c6.png" width="150">

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ し、__いくつかのマス__ に __着手__ を __行った後__ で、__リセットボタン__ を __クリック__ することで、__問題__ が __解決__ されたことを __確認__ して下さい。なお、実行結果は省略します。

```python
mb.play(ai=[None, None], gui=True)
```

__上記の修正__ で、__リセットボタン__ の __処理__ が __実装__ できた __思う人__ が __いるかもしれません__ が、実は __上記__ のプログラムには __問題があります__。それが何かを少し考えてみて下さい。

## リセットボタンの処理の問題点

__上記__ のプログラムの __問題点__ は、__先手__ である __〇__ を __AI が担当__ した際に、__リセット後__ に __AI__ が __着手を行わない__ というものです。__下記__ の __`ai2`__ VS __`ai2`__ のプログラムを __実行__ すると、__実行結果__ のように、__AI どうし__ の __対戦結果__ が __表示__ されます。この __処理__ には __問題__ は __ありません__。

```python
from ai import ai2

mb.play(ai=[ai2, ai2], gui=True)
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4d326df2-daa7-1b93-f9c7-4cae1b7dd500.png" width="150">

__下図__ は、__上記__ の __実行結果__ で、__リセットボタン__ を __クリック__ した場合の __図__ です。__図__ のように、__AI どうし__ の対戦であるにも __関わらず__、__着手__ が __行われない__ ことが __確認__ できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e05aec7b-7aa7-57ca-82ce-fdde1bb94189.png" width="150">

また、__上図__ の __ゲーム盤の上__ で __マウスを押す__ と、そのマスに __着手__ が __行われた後__ で、__× を担当__ する __AI__ が __着手__ を __行います__。__下図__ は、__上図__ で __(1, 1)__ に __着手__ を __行った場合__ の図です。図のように、__× を担当__ する __AI__ が __(2, 2)__ のマスに __着手__ を __行います__。なお、__`ai2`__ は __ランダムな着手__ を行うので、__下図__ とは __異なるマス__ に __着手__ が行われる __場合__ が __あります__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/09d653eb-436d-8a8a-ba99-665360724a94.png" width="150">

__以後__ も、__ゲーム盤の上__ で __マウス__ を押して __〇 の着手__ を __行う__ と、__× を担当__ する __AI__ が __着手__ を __行います__。このように、__最初__ は __AI どうし__ の __対戦__ を行っていたにも __関わらず__、__リセットボタン__ を __クリック__ すると、__人間__ VS __AI__ の __対戦__ が __行われる__ という __問題が発生__ しています。

__上記__ の __問題点__ を __まとめる__ と __以下__ のようになります。

* __AI VS AI__ の __対戦__ を行った場合、__リセットボタン__ の __クリック後__ に __AI が着手__ を __行わない__
* __ゲーム盤の上__ で __マウスを押す__ と、__人間__ が __〇 の 着手__ を __行えてしまう__
* __人間__ が __〇 の着手__ を __行った後__ で、__AI__  が __× の着手__ を __行う__
* __その後__ も、__人間__ が __〇 の着手__ を __行えてしまう__
* 結果として、__人間__ VS __AI__ の __対戦__ が __行われてしまう__

何故このようなことが起きるかについて少し考えてみて下さい。

### リセットボタンの下に表示される文字列の削除

上記の問題の原因を説明する前に、__リセットボタン__ の __下に表示__ される __文字列__ について __説明__ します。__リセットボタン__ の __下に表示__ される __`'x'`__ は、__× が勝利__ したことを表す __`play`__ メソッドの __返り値__ です。[以前の記事](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#jupyterlab-%E3%82%92%E5%86%8D%E8%B5%B7%E5%8B%95%E3%81%99%E3%82%8B)のノートで説明したように、__JupyterLab__ では、__セルの最後__ に __記述__ された __文__ の __計算結果__ が __表示される__ ようになっているので、__`play`__ メソッドの __返り値__ が __下図__ のように __リセットボタンの下__ に __表示__ されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4d326df2-daa7-1b93-f9c7-4cae1b7dd500.png" width="150">

__`play`__ メソッドの __返り値__ を __表示したくない__ 場合は、__下記__ のプログラムのように、__セル__ の __最後の文__ の __後__ に __`;` を記述__ します。__`;`__ は、__文を区切る__ ための __記号__ で、__`;` の後__ に __何も記述しない__ ことで、__セルの最後__ に __記述__ された __文__ が __空の文__ に __なります__。__セルの最後__ で __空の文__ を __実行__ した場合は __何も表示されない__ ので、__下記__ のプログラムのように __`;` の前__ に __記述__ した __`play`__ メソッドの __計算結果__ が __表示されなくなります__。

```python
mb.play(ai=[ai2, ai2], gui=True);
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0a9c0b07-de15-bbb9-1e0a-a0ec8d3dcb23.png" width="150">

__以後__ は、__余計な表示__ を __行いたくない__ 場合に、__セル__ の __最後の文の後__ に __`;` を記述__ します。

:::note
実は、__`play`__ メソッドの __返り値など__ は、__これまでのプログラム__ でも __表示されていました__ が、あまり __目立たなかった__ ので、本記事の __実行結果__ に __表記しません__ でした。

__リセットボタン__ を __表示__ した場合は、__リセットボタンの下__ に __表示される__ ため、__目立ってしまう__ ので、上記の方法を使って __表示しないようにする__ ことにしました。
:::

### リセットボタンのクリック後に AI が着手を行わない問題の原因

__下記__ は、__リセットボタン__ を __クリック__ した際に __実行__ される __イベントハンドラ__ です。

```python
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
        self.draw_board(ax)   
```

この __イベントハンドラ__ の __処理__ は、「__ゲーム__ の __リセット__」と「__ゲーム盤__ の __描画の処理__」を行うと __終了__ します。そのため、__リセットボタン__ を __クリック__ した __後__ に、__AI__ が __〇 を担当__ していた場合でも、__AI__ が __着手__ の __処理を行う__ ことは __ありません__。

### 人間が 〇 の着手を行えてしまう問題の原因

__リセットボタン__ を __クリック__ した場合に __実行__ される __イベントハンドラ__ の __処理が終了__ すると、__イベントループ__ の __処理が再開__ されます。そのため、__マウスを押す__ と __`on_mouse_down`__ の __イベントハンドラ__ が __実行__ され、__マウス__ を __押したマス__ に __着手を行う__ 処理が __実行__ されます。これが、__人間__ が __〇 の着手__ を __行えてしまう問題__ の __原因__ です。

### 人間が 〇 の着手を行った後で AI が × の着手を行う原因

__ゲーム盤の上__ で __マウスを押す__ と、__下記__ の __イベントハンドラ__ が __実行__ されます。

```python
 1  def on_mouse_down(event):
 2      # Axes の上でマウスを押していた場合のみ処理を行う
 3      if event.inaxes and self.status == Marubatsu.PLAYING:
 4          x = math.floor(event.xdata)
 5          y = math.floor(event.ydata)
 6          self.move(x, y)                
 7          self.draw_board(ax)
 8
 9          # 現在の手番を表す ai のインデックスを計算する
10          index = 0 if self.turn == Marubatsu.CIRCLE else 1
11          # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
12          if self.status == Marubatsu.PLAYING and ai[index] is not None:                
13              x, y = ai[index](self, **params[index])
14              self.move(x, y) 
15              self.draw_board(ax)
```

この __イベントハンドラ__ は、__マウスが押されたマス__ に __着手__ を __行った後__ で __次の手番__ を __AI が担当__ していた場合は、__12 ~ 15 行目__ で、__AI__ が __着手を行う処理__ を __行います__。__先程 `play`__ メソッドで __開始__ したゲームは、__`ai2`__ VS __`ai2`__ だったので、__6 行目__ の __〇__ の __着手後__ に __× を担当__ する __AI が着手__ を行い、この __イベントハンドラの処理__ は __終了__ します。これが、__人間__ が __〇 の着手__ を __行った後__ で __× を担当__ する __AI__ が __着手を行う原因__ です。

### その後も人間が 〇 の着手を行えてしまう原因

__上記の処理__ を __行った後__ で、__AI__ が __着手を行う処理__ は __記述されていない__ ので、__次__ の __〇 の着手__ を __AI が行う__ ことは __ありません__。また、__`on_mouse_down`__ の __処理__ は __終了する__ ので、__イベントループ__ の __処理が再開__ します。そのため、__先程__ と __同じ理由__ で、__その後__ も __人間__ が __〇 の着手__ を __行えてしまいます__。

### 結果として 人間 VS AI の 対戦が行われてしまう問題の原因

上記から、__人間__ が __〇 の着手__ を __行うたび__ に __× を担当__ する __AI__ が __着手__ を行い、再び __人間__ が __〇 の着手__ を __行うことができる__ ようになります。そのため、__結果__ として __人間__ VS __AI__ の __対戦__ が行われます。__以上__ が __問題の原因__ です。

### 処理のフローチャート

下図は、__リセットボタン__ を __押した場合__ に呼び出される __`on_button_clicked`__ の __処理__ の __フローチャート__ です。図から、__AI__ が __着手を行わない__ ことが __わかります__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/2298ca30-f6c7-41eb-cda2-a239505f2d33.png" width="150">

下図は、__その後__ に __ゲーム盤の上__ で __マウス__ を __押すたび__ に __呼び出される__ __`on_mouse_down`__ の __処理__ の __フローチャート__ です。実際に行われる __赤線の処理__ からわかるように、__人間__ の __〇 の着手__ が __行われた後__ で、__AI__ が __× の着手__ を行い、__処理が終了__ します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e7e0271b-e729-bebc-5241-5ef16ad80119.png" width="250">

__問題の原因__ が __わかりました__ ので、__問題__ を __解決する方法__ について少し考えてみて下さい。

## 問題の解決方法

__初心者__ の方は、下記の __間違った__ 問題の __解決方法__ を __思いつく人__ が __多いのではないか__ と思いますので、__間違った__ 問題の __解決方法__ を __紹介__ してから、__正しい解決方法__ を __説明__ します。

### 間違った問題の解決方法

__上記__ の現象は、[前回の記事](https://qiita.com/ysgeso/items/de4199ac562c694fb679#%E4%BA%BA%E9%96%93-vs-ai-%E3%81%AE%E5%AF%BE%E6%88%A6)で、__人間__ VS __AI__ の __処理__ を __実装する際__ に __起きた__、__人間__ が __着手を行った後__ に __AI__ が __着手を行わない__ という現象と __似ています__ です。その際には、__`on_mouse_down`__ が __着手の処理__ を __行った後__ で、「__次の手番__ を __AI が担当__ する場合は __その AI__ が __着手を選択__ する」という __処理を記述__ することで __問題を解決__ しました。

下記は、__`on_mouse_down`__ の __その部分の処理__ を __緑色で囲った__ フローチャートです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3fa40764-980f-3887-a724-9d4005ba8ed1.png" width="250">

そのため、__同様の考え方__ で、__リセットボタン__ を __クリック__ した時の __イベントハンドラ__ に、__下記__ のプログラムのように __ゲーム__ の __リセットの処理__ を __行った後__ で「__AI の手番__ の場合は、__その AI__ が __着手を選択__ する」__処理を記述__ すればよいと __考えた人__ はいないでしょうか？

* __8 ~ 13 行目__：__リセットボタン__ を __クリック__ した場合の __イベントハンドラ__ の __最後__ に、__ゲーム盤の上__ で __マウス__ を __押した場合__ の __`on_mouse_down`__ の __イベントハンドラ__ の __中__ の、__現在の手番__ が __AI の場合__ に __AI__ が __着手を行う処理__ と __同じ処理__ を __そのまま記述__ する

```python
 1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略  
 2      # リセットボタンのイベントハンドラを定義する
 3      def on_button_clicked(b):
 4          self.restart()       
 5          self.draw_board(ax)  
 6       
 7          # 現在の手番を表す ai のインデックスを計算する
 8          index = 0 if self.turn == Marubatsu.CIRCLE else 1
 9          # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
10          if self.status == Marubatsu.PLAYING and ai[index] is not None:                
11              x, y = ai[index](self, **params[index])
12              self.move(x, y) 
13              self.draw_board(ax)                               
元と同じなので省略
14    
15  Marubatsu.play = play
```

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

```python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # 〇×ゲームを再起動する
    self.restart()
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
        self.draw_board(ax)  
        
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
        if self.status == Marubatsu.PLAYING and ai[index] is not None:                
            x, y = ai[index](self, **params[index])
            self.move(x, y) 
            self.draw_board(ax)                               
         
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)

                # 現在の手番を表す ai のインデックスを計算する
                index = 0 if self.turn == Marubatsu.CIRCLE else 1
                # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
                if self.status == Marubatsu.PLAYING and ai[index] is not None:                
                    x, y = ai[index](self, **params[index])
                    self.move(x, y) 
                    self.draw_board(ax)
                   
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.draw_board(ax)
        else:
            print(self)
            
    return self.status
    
Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略  
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
        self.draw_board(ax)  
        
        # 現在の手番を表す ai のインデックスを計算する
+       index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
+       if self.status == Marubatsu.PLAYING and ai[index] is not None:                
+           x, y = ai[index](self, **params[index])
+           self.move(x, y) 
+           self.draw_board(ax)                               
元と同じなので省略
    
Marubatsu.play = play
```
</details>

__下図左__ は、__`on_button_clicked`__ の __追加した部分__ を __緑色で囲った__ フローチャートです。__下図右__ の __`on_mouse_down`__ のフローチャートと __見比べて__ みて下さい。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d163bd21-14c6-05bf-ed66-99bf4a577d10.png" width="225">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3fa40764-980f-3887-a724-9d4005ba8ed1.png" width="250">

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、先程と同様に、__AI どうし__ の __試合結果__ が __表示__ されます。

```python
mb.play(ai=[ai2, ai2], gui=True);
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0a9c0b07-de15-bbb9-1e0a-a0ec8d3dcb23.png" width="150">

__上図__ で __リセットボタン__ を __クリック__ すると、__今度__ は __〇 の手番__ を __担当する AI__ が __着手を行い__、__下図__ のような __画像__ が __表示__ されます。しかし、__下図__ からわかるように、__その次__ の __× を担当__ する __AI__ が __着手を行わない__ という __問題が発生__ します。なお、`ai2` はランダムな着手を行うので下図とは異なる場合があります

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/801a494a-740d-e8c3-ccf1-735a04413897.png" width="150">

図は示しませんが、__上図__ で __ゲーム盤__ の __画像の上__ で __マウスを押す__ と、__× の着手__ が __行われた後__ で、__AI__ が __〇 の着手__ を __行います__。従って、__この場合__ は、__AI__ VS __人間__ の __対戦__ が __行われる__ ことになります。何故このようなことが起きるかについて少し考えてみて下さい。

### 問題の検証と修正

下図は、__リセットボタン__ を __押した場合__ に呼び出される __`on_button_clicked`__ の __処理__ の __フローチャート__ です。図から、__AI__ が __〇 の着手のみ__ を __行う__ ことが __わかります__。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d2084772-fbf6-97f4-d633-125bf2708239.png" width="250">

下図は、__その後__ に __ゲーム盤の上__ で __マウス__ を __押すたび__ に __呼び出される__ __`on_mouse_down`__ の __処理__ の __フローチャート__ です。実際に行われる __赤線の処理__ からわかるように、__人間__ の __× の着手__ が __行われた後__ で、__AI__ が __〇 の着手__ を行い、__処理が終了__ します。なお、__下図__ は、先程の __人間__ VS __AI__ の __場合__ に、__`on_mouse_down`__ が __呼び出され__ た際に __行われる処理__ と __同じ__ です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e7e0271b-e729-bebc-5241-5ef16ad80119.png" width="250">　

この問題は、__リセットボタン__ を __クリック__ した際の __イベントハンドラ__ である __`on_button_clicked`__ が、__AI の着手__ を __行う処理__ を __1 度しか行わない__ 点にあります。__AI どうし__ の __対戦__ の場合は、__下記__ の __フローチャート__ のように、__1 度ではなく__、__ゲーム__ の __決着がつくまで__ 繰り返し __AI の着手__ を __行う必要__ が __あります__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/9d8fd9ec-c852-ecc9-536f-8dedda212390.png" width="350">

__下図左__ は、__AI どうし__ の __対戦__ を行った場合の __`on_button_clicked`__ の __処理__ を __赤線__ と __紫色__ の __線__ で示した __フローチャート__ です。__赤色__ の線は、__決着がついていない__ 場合、__紫色__ の線は __決着がついた__ 場合の __処理の流れ__ です。__下図右__ の、__修正前__ の __`on_button_clicked`__ の __フローチャート__ と __見比べて__ みて下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/34673f15-236f-5bb2-6116-85a82d2bddb7.png" width="350">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/808940b3-7a19-8406-bb32-00c0b2117b1a.png" width="200">

__下記__ は __上記__ の __フローチャート__ のように __`on_button_clicked`__ を __修正__ したプログラムです。

* __7 行目__：__ゲーム__ の __決着がつくまで__ 繰り返し __処理を行う__
* __11 ~ 14 行目__：__AI の手番__ の場合は、__AI__  が __着手を選択__ する。なお、__元__ のプログラムの __11 行目__ の __条件式__ には、__`self.status == Marubatsu.PLAYING`__ が __記述__ されていたが、__その条件式__ が __満たされること__ は、__7 行目__ の __while 文__ の __条件式__ で  __保証されている__ ので、__11 行目__ の __条件式から削除__ した
* __17 行目__：__人間の手番__ の場合は、__return__ 文を __記述__ して、__イベントハンドラ__ の __処理を終了__ することで、__イベントループを再開__ して __マウス__ による __着手__ が __行えるようにする__

```python
 1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
 2      # リセットボタンのイベントハンドラを定義する
 3      def on_button_clicked(b):
 4          self.restart()       
 5          self.draw_board(ax)  
 6       
 7          while self.status == Marubatsu.PLAYING:
 8              # 現在の手番を表す ai のインデックスを計算する
 9              index = 0 if self.turn == Marubatsu.CIRCLE else 1
10              # ai が着手を行うかどうかを判定する
11              if ai[index] is not None:                
12                  x, y = ai[index](self, **params[index])
13                  self.move(x, y) 
14                  self.draw_board(ax)                               
15              else:
16                  # 人間の手番の場合は、イベントハンドラを終了する
17                  return
元と同じなので省略         
18    
19  Marubatsu.play = play
```

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

```python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # 〇×ゲームを再起動する
    self.restart()
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
        self.draw_board(ax)  
        
        while self.status == Marubatsu.PLAYING:
            # 現在の手番を表す ai のインデックスを計算する
            index = 0 if self.turn == Marubatsu.CIRCLE else 1
            # ai が着手を行うかどうかを判定する
            if ai[index] is not None:                
                x, y = ai[index](self, **params[index])
                self.move(x, y) 
                self.draw_board(ax)                               
            else:
                # 人間の手番の場合は、イベントハンドラを終了する
                return
         
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)

                # 現在の手番を表す ai のインデックスを計算する
                index = 0 if self.turn == Marubatsu.CIRCLE else 1
                # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
                if self.status == Marubatsu.PLAYING and ai[index] is not None:                
                    x, y = ai[index](self, **params[index])
                    self.move(x, y) 
                    self.draw_board(ax)
                   
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.draw_board(ax)
        else:
            print(self)
            
    return self.status
    
Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
        self.draw_board(ax)  
        
+       while self.status == Marubatsu.PLAYING:
            # 現在の手番を表す ai のインデックスを計算する
            index = 0 if self.turn == Marubatsu.CIRCLE else 1
            # ai が着手を行うかどうかを判定する
-           if self.status == Marubatsu.PLAYIN and ai[index] is not None:                
+           if ai[index] is not None:                
                x, y = ai[index](self, **params[index])
                self.move(x, y) 
                self.draw_board(ax)                               
+           else:
                # 人間の手番の場合は、イベントハンドラを終了する
+               return
元と同じなので省略         
    
Marubatsu.play = play
```
</details>

実行結果は省略しますが、__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、今度は __リセットボタン__ を __クリック__ する __たび__ に __AI どうし__ の __対戦__ が __行われる__ ことが __確認__ できます。本記事では省略しますが、__人間__ VS __人間__、__人間__ VS __AI__、__AI__ VS __人間__ の __対戦__ でも __正しく動作__ することを、__確認__ してください。

```python
mb.play(ai=[ai2, ai2], gui=True);
```

# 今回の記事のまとめ

今回の記事では、__ipywidgets__ の __ボタン__ を __利用__ することで、__リセットボタン__ を __表示__ して、__〇×ゲーム__ の __リセット__ を行うことが __できるように__ しました。

ただし、__今回の記事__ で __記述__ した __プログラム__ には、__同じような内容__ のプログラムが __複数の個所__ で __重複__ しているという __問題がある__ ので、次回の記事では __その問題__ を __修正__ します。

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

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

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

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

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

# 次回の記事

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

[^1]:ここでいう __インターフェース__ は、ユーザーインターフェースの場合と同様に __操作環境__ の事を表します。具体的には __pyplot__ に関する __操作を行う__ ための __メソッド__ のことを表します