Pythonで〇×ゲームのAIを一から作成する　その78　リプレイ機能の実装

# 目次と前回の記事

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

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

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

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

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

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

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

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

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

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

# リプレイ機能

__これまで__ のプログラムでは、__`play`__ メソッドで __AI どうし__ の __対戦__ を __GUI__ で __行った場合__ に、__AI__ が __すべての着手__ を __行った後__ の、__決着がついた__ ゲーム盤が __表示__ されていました。今回の記事では、__対戦__ の __途中経過__ を __表示__ する、__リプレイ機能__ を __実装__ することにします。

__リプレイ機能__ を __実装__ するためには、リプレイを行うための、__具体的な機能__ について __検討__ する必要があります。__どのような機能__ が __必要か__ について少し考えてみて下さい。

# リプレイに必要な機能

ぱっと思いつく __リプレイ__ に __必要な機能__ には、__下記__ のような機能があるでしょう。他にも必要な機能を思いついた方は、実装してみて下さい。

* __ゲーム開始時__ まで __戻す__ 機能
* __1 手戻す__ 機能
* __1 手進める__ 機能
* __ゲームの終了時__ まで __進める__ 機能

なお、実装を進めていけばすぐに気づくことができると思いますが、__リプレイ機能__ は、__ゲームの決着__ が __ついていない場合__ も行えた方が __便利__ です。そこで、上記の __最後の機能__ を「__最後__ に __行われた着手__ まで __進める__ 機能」に __変更__ することにします。

次に、__それぞれの機能__ を呼び出すための __UI__ を __決める必要__ が __あります__。それについても少し考えてみて下さい。

# リプレイ機能の UI の設定と実装

__最も簡単__ な __UI__ としては、__それぞれの機能__ をリセットボタンなどと同様に、__ボタンで呼び出す__ というものでしょう。__本記事__ でも __ボタンの UI__ を __採用__ することにします。

次に、__ボタン__ に __表示__ する __文字列__ と、__ボタンの配置__ を __考える必要__ が __あります__。それらについても少し考えてみて下さい。

## ボタンの表記と配置例

本記事では、それぞれの __ボタン__ に __表示__ する __文字列__ を、__動画プレーヤーなど__ で良く見かける __表記__ に __倣って__、__下記__ のような __文字列__ とし、__4 つのボタン__ を __横に並べて配置__ することにします。また、__4 つのボタン__ は、__これまでに表示__ していた、__Dropdown__ や __リセットボタン__ の __下に配置__ することにします。他の表記が良いと思った人は自由に変更してください。

| 機能 | 表記 |
|:--|:-:|
| __ゲーム開始時まで戻す__  | <<  |
| __1 手戻す__  | <  |
| __1 手進める__  | >  |
| __最後に行われた着手まで進める__  | >>  |

## リプレイ機能のウィジェットの作成と配置

上記の __4 つのボタン__ の __ウィジェット__ は、__リセットボタン__ と __同様の方法__ で __作成__ することが __できます__。その際に、__それぞれ__ の __ウィジェット__ を __代入__ する __変数の名前__ を __下記__ の表のように __名づける__ ことにします。なお、__`prev_button`__ の __prev__ は、「__前の__」という __意味__ を表す __previous の略__ です。

| 機能 | 表記 | 変数名 |
|:--|:-:|:--|
| __ゲーム開始時まで戻す__  | <<  | `first_button` |
| __1 手戻す__  | <  | `prev_button` |
| __1 手進める__  | >  | `next_button` |
| __最後に行われた着手まで進める__  | >>  | `last_button` |

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

上記の __4 つのボタン__ の __ウィジェット__ は、__Dropbox__ や __リセットボタン__ を __横に並べて配置__ するために利用した、__HBox__ を使って __横に配置__ します。また、__Dropbox など__ の __HBox__ と __リプレイのボタン__ の __HBox__ は、[以前の記事](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#hbox-%E3%81%A8-vbox-%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)で説明したように、__VBox__ を使って __縦に並べて配置__ することが __できます__。

__下記__ は、__リプレイ機能__ の __ボタン__ を __作成__ して __配置__ するように __`play`__ メソッドを __修正__ したプログラムです。

* __ウィジェット__ を __配置__ して __表示__ する __処理__ を __一か所にまとめる__ ために、__7 行目の下__ にあった __HBox__ を __作成__ して __表示__ する __処理__ を __43 行目__ 以降に __移動__ する
* __25 ~ 40 行目__：__リプレイ機能__ の __4 つのボタン__ を __作成__ し、__変数に代入__ する
* __43 行目__：__Dropdown__ と __リセットボタン__ を __横に並べて配置__ する __HBox__ を __作成__ する
* __45 行目__：__リプレイ機能__ の __ボタン__ を __横に並べて配置__ する __HBox__ を __作成__ する
* __47 行目__：__2 つ__ の __HBox__ を __縦に並べて配置__ する __VBox__ を __作成__ し、__表示__ する

```python
 1  from marubatsu import Marubatsu
 2  import matplotlib.pyplot as plt
 3  import ipywidgets as widgets
 4  import math
 5
 6  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):              # 変更ボタンのイベントハンドラを定義する
元と同じなので省略
 7          # この下にあった hbox を作成するプログラムを 43 行目に移動する
 8
 9          def on_change_button_clicked(b):
10              for i in range(2):
11                  ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
12              self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
13
14          # リセットボタンのイベントハンドラを定義する
15          def on_reset_button_clicked(b):
16              self.restart()
17              on_change_button_clicked(b)
18
19          # イベントハンドラをボタンに結びつける
20          change_button.on_click(on_change_button_clicked)
21          reset_button.on_click(on_reset_button_clicked)        
22        
23          # 2 行目の UI を作成する
24          # リプレイのボタンを作成する
25          first_button = widgets.Button(
26              description="<<",
27              layout=widgets.Layout(width="100px"),
28          )
29          prev_button = widgets.Button(
30              description="<",
31              layout=widgets.Layout(width="100px"),
32          )
33          next_button = widgets.Button(
34              description=">",
35              layout=widgets.Layout(width="100px"),
36          )
37          last_button = widgets.Button(
38              description=">>",
39              layout=widgets.Layout(width="100px"),
40          )
41        
42          # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
43          hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
44          # リプレイ機能のボタンを横に配置した HBox を作成する
45          hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
46          # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
47          display(widgets.VBox([hbox1, hbox2]))   
元と同じなので省略  
48
49  Marubatsu.play = play        
```

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

```python
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math

def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "〇" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # 変更ボタンを作成する
        change_button = widgets.Button(
            description="変更",
            layout=widgets.Layout(width="100px"),
        )
        
        # リセットボタンを作成する
        reset_button = widgets.Button(
            description="リセット",
            layout=widgets.Layout(width="100px"),
        )
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = widgets.Button(
            description="<<",
            layout=widgets.Layout(width="100px"),
        )
        prev_button = widgets.Button(
            description="<",
            layout=widgets.Layout(width="100px"),
        )
        next_button = widgets.Button(
            description=">",
            layout=widgets.Layout(width="100px"),
        )
        last_button = widgets.Button(
            description=">>",
            layout=widgets.Layout(width="100px"),
        )
        
        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    else:
        ax = None

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math

def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略       
-       # 〇 と × の dropdown とボタンを横に配置した HBox を作成し、表示する
-       hbox = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
-       display(hbox)
            
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)

        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)             
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
+       first_button = widgets.Button(
+           description="<<",
+           layout=widgets.Layout(width="100px"),
+       )
+       prev_button = widgets.Button(
+           description="<",
+           layout=widgets.Layout(width="100px"),
+       )
+       next_button = widgets.Button(
+           description=">",
+           layout=widgets.Layout(width="100px"),
+       )
+       last_button = widgets.Button(
+           description=">>",
+           layout=widgets.Layout(width="100px"),
+       )
        
        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
+       hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
+       hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
+       display(widgets.VBox([hbox1, hbox2]))            
元と同じなので省略  

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`gui_play`__ を __実行__ すると、__実行結果__ のように、__Dropdown の下__ に __リプレイ機能__ の __4 つのボタン__ が __並んで表示__ されるようになります。

```python
from util import gui_play

gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/13caff1d-20bd-23b1-0d52-1b314f8dd340.png" width="250">

### ボタンを作成する関数の定義

__上記__ のプログラムでは、__ボタン__ を __作成するたび__ に __下記__ のような __複数行__ のプログラムを __記述する必要__ があり、__記述が面倒__、__プログラム__ が __長くなる__ などの __問題__ が __あります__。

```python
        first_button = widgets.Button(
            description="<<",
            layout=widgets.Layout(width="100px"),
        )
```

__ボタンごと__ の __違い__ は、__キーワード引数 `description`__ の __値だけ__ です。そこで、__ボタン__ を __作成する処理__ を行う __下記__ のような __関数を定義__ することにします。なお、__ボタンの横幅__ を __設定できる__ と __便利__ なので、__横幅__ も __実引数__ で __設定できる__ ようにしました。

__名前__：__ボタンを作成する__ ので __`create_button`__ とする
__処理__：__指定__ した __表記__ と __横幅__ の __ボタン__ の __ウィジェット__ を __作成__ する
__入力__：__仮引数 `description`__ に __ボタン__ の __表示内容__ を、__`width`__ に __ボタン__ の __横幅__ を __代入__ する
__出力__：__作成__ した __ウィジェット__

__この関数__ を __`Marubatsu`__ クラスの __メソッド__ として __定義__ する事は __可能__ ですが、__本記事__ では __下記の理由__ から __`play`__ メソッドの __ローカル関数__ として __定義__ する事にします。

* __仮引数 `self`__ を __利用しない__
* __この処理__ は __`play`__ メソッドの __中でしか利用しない__

:::note
__`play`__ メソッド __以外の場所__ で __利用__ する __可能性がある__ 場合は、__`Marubatsu`__ クラスの __[静的メソッド](https://qiita.com/ysgeso/items/ad5e7ddb071653fd2ac5#%E9%9D%99%E7%9A%84%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)__ として __定義しても良い__ でしょう。

他にも、__通常の関数__ として __定義__ し、__util.py__ に __保存__ するという方法もあります。
:::

下記は、__`create_button`__ の __定義を追加__ して __修正__ した __`play`__ メソッドです。

* __3 ~ 7 行目__：__`create_button`__ を __`play`__ メソッドの __ローカル関数__ として __定義__ する
* __10、11、14 ~ 17 行目__：__`create_button`__ を __利用__ して __ボタンを作成__ するように __修正__ する

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略       
 2          # ボタンを作成するローカル関数を定義する 
 3          def create_button(description, width):
 4              return widgets.Button(
 5                  description=description,
 6                  layout=widgets.Layout(width=f"{width}px"),
 7              )
 8       
 9          # 変更、リセットボタンを作成する
10          change_button = create_button("変更", 100)
11          reset_button = create_button("リセット", 100)
元と同じなので省略       
12          # 2 行目の UI を作成する
13          # リプレイのボタンを作成する
14          first_button = create_button("<<", 100)
15          prev_button = create_button("<", 100)
16          next_button = create_button(">", 100)
17          last_button = create_button(">>", 100)
元と同じなので省略       

Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "〇" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    else:
        ax = None

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略       
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
元と同じなので省略       
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
元と同じなので省略       

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

実行結果は同じなので省略しますが、__上記__ の __修正後__ に、__下記__ のプログラムで __`gui_play`__ を __実行__ すると __正しい処理__ が __行われる__ ことが __確認__ できます。

```python
gui_play()
```

# リプレイ機能の実装

__UI__ が __表示される__ ようになったので、次は __それぞれのボタン__ を __クリックした際に実行__ される __イベントハンドラ__ を __定義__ する __必要__ が __あります__。__イベントハンドラ__ に __どのような処理__ を __記述__ すれば良いかについて少し考えてみて下さい。

## リプレイ機能が行う処理

__リプレイ機能__ では、__4 種類__ の __ボタン__ を押すことで、__ゲーム__ が __開始され__ てから __終了するまで__ の __任意の手数__ の __ゲーム盤__ を __表示__ します。そのため、__リプレイ機能__ を __実装__ するためには、__任意の手数__ が __指定__ された際に、__その手数__ の __ゲーム盤を表示__ するという __処理__ を __行う必要__ が __あります__。そこで、__そのような処理__ を行う __下記__ の __関数を定義__ する事にします。

__名前__：__表示__ する __ゲーム__ の __手数__（step）を __変更__（change）するので __`change_step`__ とする
__処理__：__指定__ した __手数__ の __ゲーム盤__ の __画像を描画__ する
__入力__：__仮引数 `step`__ に __手数__ を __代入__ する
__出力__：なし

__`change_step`__ を __どのように実装__ すればよいかについて少し考えてみて下さい。

## ゲーム盤のデータを記録する方法

__任意の手数__ の __ゲーム盤__ を __描画__ するためには、__その手数__ の __ゲーム盤__ を __再現__ する __必要__ があります。__その方法__ の __一つ__ に、__着手__ を __行うたび__ に、__ゲーム盤__ を表す __`borad`__ 属性の __データ__ を __コピー__ して __記録しておく__ という方法があり、__以下__ のような __方法__ で __実装__ できます。

* __各手数__ の __ゲーム盤の情報__ を __記録__ するための __属性の名前__ を __考える__
* __ゲーム__ を __リセット__ する __処理__ を行う __`restart`__ メソッドの __中__ で、__その属性__ に __空の list__ を __代入__ して __初期化__ する
* __着手__ を行う __`move`__ メソッドの __中__ で、__その属性__ の __要素__ に、__着手後__ の __`board`__ 属性の __内容__ を __`deepcopy(self.board)`__ で __[深いコピー](https://qiita.com/ysgeso/items/ff28a95dff50322e6b16#%E6%B7%B1%E3%81%84%E3%82%B3%E3%83%94%E3%83%BC)__ を行ったものを __追加__ する

__ゲーム盤__ の __データ__ を __記録する際__ は、代入や浅いコピーではなく、__深いコピー__ を __行わなければならない__ 点に __注意__ して下さい。

__代入__ では __うまくいかない理由__ は、__list__ に __要素を追加__ する際に行われる __[代入](https://qiita.com/ysgeso/items/cc9e67c16b0814c8bc2e#%E3%81%BE%E3%81%A8%E3%82%81)__ が __オブジェクト__ を __共有__ するという __処理__ だからです。__着手__ を行うと __`board`__ 属性に __代入__ された __list__ の __要素の値__ は __変化__ しますが、__`board`__ 属性に __代入__ される __オブジェクト__ は __変化しません__。そのため、__着手__ を __行うたび__ に __list__ に __`board`__ 属性を __追加__ すると、__すべての要素__ に __同一のオブジェクト__ が __代入される__ ことになります。

__浅いコピー__ では __うまくいかない理由__ は、__`board`__ 属性の __要素の値__ が __ミュータブル__ なデータである __list__ だからです。忘れた方は、[以前の記事](https://qiita.com/ysgeso/items/ff28a95dff50322e6b16#%E6%B5%85%E3%81%84%E3%82%B3%E3%83%94%E3%83%BC%E3%81%AE%E6%80%A7%E8%B3%AA)を __復習__ して下さい。

:::note
__任意の手数__ の __ゲーム盤__ を __再現__ する __他の方法__ は、__今後の記事__ で __説明します__。
:::


### `record_boards` 属性の実装

__下記__ は、__属性の名前__ を __`record_boards`__ という __名前__ にした場合の __実装例__ です。__それぞれ__ の __メソッド__ の __最後の行__ が __追加__ した __プログラム__ です。プログラムを見ればわかると思いますが、__行っている処理__ は、__着手を記録__ する __`records`__ 属性の __処理__ と __同様__ です。

```python
from copy import deepcopy

def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    self.last_move = -1, -1          
    self.last_turn = None
    self.records = []
    self.board_records = []

Marubatsu.restart= restart

def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.last_turn = self.turn
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
        self.status = self.judge()
        self.last_move = x, y
        self.records.append(self.last_move)
        self.board_records.append(deepcopy(self.board))
        
Marubatsu.move = move
```

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

```diff_python
from copy import deepcopy

def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    self.last_move = -1, -1          
    self.last_turn = None
    self.records = []
+   self.board_records = []

Marubatsu.restart= restart

def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.last_turn = self.turn
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
        self.status = self.judge()
        self.last_move = x, y
        self.records.append(self.last_move)
+       self.board_records.append(deepcopy(self.board))
        
Marubatsu.move = move
```
</details>

:::note
__上記__ のプログラムで、__`records`__ 属性に __追加__ する __`self.last_move`__ を __深いコピー__ で __コピーするべき__ ではないかと __思った方__ が __いるかもしれない__ ので __補足__ します。

__`self.last_move`__ の __値__ は、__直前__ の __`self.last_move = x, y`__ で、__新しい tuple__ が __代入__ されるので、コピーを行わなくても __`records`__ 属性の __他の要素__ と __値が共有される__ ことは __ありません__。それに対し、__`self.board`__ は __着手__ を行っても __同じオブジェクト__ を __参照し続ける__ ので、__深いコピー__ を __行わず__ に __`board_records` に追加__ すると、__`board_records`__ の __全ての要素__ に __同じオブジェクト__ が __代入される__ ことになります。
:::

:::note
__上記__ のプログラムでは、__着手__ を記録する __属性__ と、__ゲーム盤__ を記録する __属性__ を __別々の属性__ にしましたが、__下記__ のプログラムのように、__tuple__ や __dict__ を使って、__`records` 属性__ に __まとめて記録__ することもできます。なお、__下記__ のプログラムは、__`move`__ メソッド内で、__記録を行う処理__ の __部分__ です。


```python
# tuple を利用する場合
self.records.append((self.last_move, deepcopy(self.board)))
# dict を利用する場合
self.records.append({
    "move": self.last_move,
    "board": deepcopy(self.board))
})
```

ただし、__上記__ のように __`records`__ の __要素__ に __代入__ する __データの形式__ を __変更__ すると、__`records`__ 属性を __利用する__ これまでの __プログラム__ が __動作しなくなる__ ので、__本記事__ では __上記の方法__ は __採用しない__ ことにします。
:::

### `change_step` の実装

下記は、__`change_step`__ の __定義__ です。

* __3 行目__：__`step`__ 手目の __ゲーム盤__ の __データ__ を __`board_records`__ から __取り出し__、__深いコピー__ を行ったデータを __`board` に代入__ する
* __5 行目__：__`draw_board`__ を使って、__ゲーム盤の描画__ を __更新__ する

```python
1  def change_step(step):
2      # step 手目のゲーム盤のデータをコピーし、board に代入する
3      self.board = deepcopy(self.board_records[step])
4      # 描画を更新する
5      self.draw_board(ax, ai)
```

:::note
__3 行目__ を __`self.board = self.board_records[step]`__ のように __データ__ を __コピーせず__ に __代入する__ と __データ__ が __共有される__ ので __バグが発生__ します。__具体的__ には、__着手__ を行って __`self.board`__ の __要素の値__ が __変化__ すると __`self.board_records[step]`__ の __要素の値__ も __同時__ に __変更__ されてしまいます。
:::

次に、__この関数__ を __どこに記述する__ かを __考える必要__ が __あります__。__この関数__ の __中__ では、__`self`__ を __利用__ しているので、__`Marubatsu`__ クラスの __メソッド__ として __定義__ する方法が考えられますが、その場合は、__`ax`__、__`ai`__ を __仮引数__ とする __必要がある__ 点が少し __面倒__ です。一方、__`play`__ メソッドの __ローカル関数__ として __定義__ すれば、__それらの名前__ を __そのまま利用__ することが __できる__ ので、本記事では __`play`__ メソッドの __ローカル関数__ として __定義__ します。

:::note
__`ax`__ や __`ai`__ を、__`play`__ メソッドの __ローカル変数__ として __ではなく__、__`self.ax`__ や __`self.ai`__ のように、__`Marubatsu`__ クラスの __属性にする__ ことで、__`change_step`__ を __`Marubatsu`__ クラスの __メソッドとして定義__ しても __仮引数 `ax`__ や __`ai`__ を __記述しなくても済む__ ようになります。ただし、今からそのようにプログラムを修正すると __大幅な修正__ が __必要になる__ ので __本記事__ では __採用しない__ ことにします。
:::

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

* __9 ~ 13 行目__：__`change_step`__ の __定義__ を __記述__ する

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          # 2 行目の UI を作成する
 3          # リプレイのボタンを作成する
 4          first_button = create_button("<<", 100)
 5          prev_button = create_button("<", 100)
 6          next_button = create_button(">", 100)
 7          last_button = create_button(">>", 100)
 8       
 9          def change_step(step):
10              # step 手目のゲーム盤のデータをコピーし、board に代入する
11              self.board = self.board_records[step]
12              # 描画を更新する
13              self.draw_board(ax, ai)
元と同じなので省略        
14
15  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "〇" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step 手目のゲーム盤のデータをコピーし、board に代入する
            self.board = deepcopy(self.board_records[step])
            # 描画を更新する
            self.draw_board(ax, ai)
        
        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    else:
        ax = None

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
+       def change_step(step):
+           # step 手目のゲーム盤のデータをコピーし、board に代入する
+           self.board = self.board_records[step]
+           # 描画を更新する
+           self.draw_board(ax, ai)
元と同じなので省略        

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

## ゲーム開始時まで戻すボタンのイベントハンドラの定義

__ゲーム開始まで戻す__ ボタンの __イベントハンドラ__ で __行う処理__ は、__1 手__ も __着手__ を __行っていない__、__0 手目__ の __ゲーム盤の画像__ を __描画する処理__ なので、__`change_turn(0)`__ によって __行う__ ことが __できます__。そこで、その __イベントハンドラ__ を __下記__ のように __`on_first_button_clicked`__ という __名前__ で __定義__ し、__ボタン__ に __結びつける__ ことにします。

* __8、9 行目__：__イベントハンドラ__ を __定義__ する
* __11 行目__：__ボタン__ に __イベントハンドラ__ を __結び付ける__

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          def change_step(step):
 3              # step 手目のゲーム盤のデータを board に代入する
 4              self.board = self.board_records[step]
 5              # 描画を更新する
 6              self.draw_board(ax, ai)
 7       
 8          def on_first_button_clicked(b):
 9              change_step(0)
10
11          first_button.on_click(on_first_button_clicked)
元と同じなので省略
12
13  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "〇" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step 手目のゲーム盤のデータを board に代入する
            self.board = self.board_records[step]
            # 描画を更新する
            self.draw_board(ax, ai)
        
        def on_first_button_clicked(b):
            change_step(0)

        first_button.on_click(on_first_button_clicked)

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    else:
        ax = None

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        def change_step(step):
            # step 手目のゲーム盤のデータを board に代入する
            self.board = self.board_records[step]
            # 描画を更新する
            self.draw_board(ax, ai)
        
+       def on_first_button_clicked(b):
+           change_step(0)

+       first_button.on_click(on_first_button_clicked)
元と同じなので省略

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`gui_play`__ を __実行__ し、__いくつかの着手__ を __行った後__ で、__<<__ ボタンを __クリック__ すると、__実行結果__ のように、__1 手目__ の __着手__ が __行われた後__ の __ゲーム盤__ が __描画__ されるという __問題が発生__ します。

```python
gui_play()
```

実行結果（下図は、1 手目に真ん中のマスに着手を行った場合の図です）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/99061c3d-a09c-234e-7cd3-179699512b14.png" width="250">

### 問題の原因の検証

__`on_first_button_clicked`__ が __行う処理__ は、__`change_step(0)` だけ__ なので、__問題の原因__ は、__`change_step(0)`__ が __行う処理__ を __調べる__ ことで __検証__ できます。__`change_step(0)`__ では、__`self.board = self.board_records[0]`__ を __実行__ するので、__`self.board_records[0]`__ が __1 手目__ の __着手__ を __行った後__ の __ゲーム盤のデータ__ であることが __推測__ されます。

__`board_record`__ 属性に対して __行われる処理__ は、以下の通りです。

* __ゲーム__ の __リセット時__ に、__空の list__ で __初期化__ する
* __着手__ を __行うたび__ に、__着手後__ の __ゲーム盤のデータ__ の __深いコピー__ を __要素__ に __追加__ する

__上記__ から、__ゲーム開始時__ の __`board_records`__ 属性の __値__ は __空の list__ なので、__1 手目__ の __ゲーム盤のデータ__ は、__`board_records`__ の __0 番__ の __要素__ に __代入__ されることが __わかります__。

このことから、__この問題__ の __原因__ は __ゲーム開始時__ の __ゲーム盤のデータ__ が __`board_records`__ 属性の __0 番__ の __要素__ に __代入されていない__ ことが __原因__ であることが __わかります__。

そのことは、__ゲームの開始直後__ に __<<__ ボタンを __クリック__ した際に、__下記__ のような __エラーが発生__ することからも __確認__ できます。__下記__ のエラーは、__空の要素__ が __代入__ されている __`board_records`__ の __0 番__ の __要素__ を __参照__ しようとしたために __発生__ します。

```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[7], line 91
     90 def on_first_button_clicked(b):
---> 91     change_step(0)

Cell In[7], line 86
     84 def change_step(step):
     85     # step 手目のゲーム盤のデータを board に代入する
---> 86     self.board = self.board_records[step]
     87     # 描画を更新する
     88     self.draw_board(ax, ai)

IndexError: list index out of range
```

### バグの修正

従って、__下記__ のプログラムの __9 行目__ のように、__`restart`__ メソッド内で、__`board_records`__ の __0 番__ の __要素__ に、__ゲーム開始時__ の __ゲーム盤のデータ__ を __コピーしたもの__ を __代入__ するようにすることで __この問題__ を __解決__ できます。

```python
 1  def restart(self):
 2      self.initialize_board()
 3      self.turn = Marubatsu.CIRCLE     
 4      self.move_count = 0
 5      self.status = Marubatsu.PLAYING
 6      self.last_move = -1, -1          
 7      self.last_turn = None
 8      self.records = []
 9      self.board_records = [deepcopy(self.board)]
10
11  Marubatsu.restart = restart
```

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

```python
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    self.last_move = -1, -1          
    self.last_turn = None
    self.records = []
    self.board_records = [deepcopy(self.board)]

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

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

```diff_python
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    self.last_move = -1, -1          
    self.last_turn = None
    self.records = []
-   self.board_records = []
+   self.board_records = [deepcopy(self.board)]

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

__上記__ の __修正後__ に、__下記__ のプログラムで __`gui_play`__ を __実行__ し、__いくつかの着手__ を __行った後__ で、__<<__ ボタンを __クリック__ すると、__実行結果__ のように、__ゲーム開始直後__ の __ゲーム盤__ が __描画__ されます。また、__ゲームの開始直後__ に __<<__ ボタンを __クリック__ しても __エラー__ が __発生しなくなる__ ことが __確認__ できます。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/edd61f96-b047-d812-39d8-14548fba6c7e.png" width="250">

__<<__ ボタンの __処理__ には __いくつかの問題__ があります。__そのうちの 1 つ__ は、__上図__ に __現れています__。どのような問題があるかについて少し考えてみて下さい。

### << ボタンの処理の問題点 その 1

__問題__ の __1 つ__ は、__上図__ のように、__ゲーム開始時__ の __手番__ が __×__ に __なる場合がある__ という点です。ただし、__<<__ ボタンを __クリック__ した時に、__手番__ が __〇 になる__ 場合も __あります__。そこで、__<<__ ボタンを __クリック__ した時に、__どのような場合__ に __× の手番__ に __なるか__ を __色々と試して調べて__ みて下さい。

### << ボタンの処理の問題点 その 2

2 つ目の問題点は、__ゲームの決着__ が __ついた場合__ に、__<<__ ボタンを __クリック__ すると、__下図__ のように __決着__ が __ついた状態__ で __ゲーム開始時__ の __ゲーム盤__ が __表示される点__ です。この問題の原因について少し考えてみて下さい。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/dad33bcd-f5f5-1cc6-25e5-ef05636f54dc.png" width="250">

### << ボタンの処理の問題点 その 3

__着手__ を行い __<<__  ボタンを __クリック__ するという __作業__ を __繰り返す__ と、__下図__ のように __すべてのマス__ が __埋まっていない__ のに __引き分け__ になるという __問題が発生__ します。これが __3 つ目__ の __問題__ です。この問題についても原因を少し考えてみて下さい。

なお、__この後__ で __<<__ ボタンを __クリック__ すると、__引き分け__ の __状態__ で __ゲーム開始時__ の __ゲーム盤__ が __表示される__ という、__2 つ目__ の __問題__ が __発生__ します。__正しい状態__ で __ゲーム__ を __開始__ したい場合は、__リセットボタン__ を __クリック__ して下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/6b2a5bff-dbf8-908d-bee9-774a8c2d3c8e.png" width="250">

### 1 つ目と 2 つ目の問題の原因の検証と修正

__試行錯誤__ を __行う__ ことで、__<<__ ボタンを __クリック__ すると、以下の状態で __ゲーム開始時__ の __ゲーム盤__ が __表示される__ ことがわかります。

* __ゲームの決着__ が __ついていない__ 場合は、__<<__ ボタンを __クリック__ した __時点の手番__ になる
* __ゲームの決着__ が __ついている__ 場合は、__決着__ が __ついたまま__ の __状態__ になる

__手番__ の情報は __`turn`__ 属性に、__ゲームの決着がついているか__ どうかは __`status`__ 属性に __代入__ されていますが、__`change_step` 内__ では、その __いずれ__ の __属性の値__ も __変化しません__。そのため、__1 つ目__ と __2 つ目__ の __問題が発生__ します。

そこで、__下記__ のプログラムのように、__`change_step`__ の中で、__`turn`__ 属性と __`status`__ 属性の __値を計算__ するという __処理__ を __記述__ することで、__1 つ目__ と __2 つ目__ の __問題__ を __解決__ できます。

* __6 行目__：__〇×ゲームは__、__偶数__ 手目が __〇 の手番__ なので、__手数__ を表す __`step`__ が __偶数の場合__ は __〇__、__そうでない場合__ は __×__ を表す __データ__ を __`turn`__ 属性に __代入__ する。__偶数かどうか__ は __余り__ を __計算__ する __% 演算子__ と、__2__ で __割った余り__ が __0 であるか__ どうかで __判定__ できる
* __8 行目__：__`judge`__ メソッドを使って __`status`__ 属性の __値__ を __計算し直す__

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          def change_step(step):
 3              # step 手目のゲーム盤のデータをコピーし、board に代入する
 4              self.board = deepcopy(self.board_records[step])
 5              # 手番を計算する。step が偶数の場合は 〇 の 手番
 6              self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
 7              # status 属性を judget を使って計算する
 8              self.status = self.judge()
 9              # 描画を更新する
10              self.draw_board(ax, ai)
元と同じなので省略
11
12  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "〇" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step 手目のゲーム盤のデータをコピーし、board に代入する
            self.board = deepcopy(self.board_records[step])
            # 手番を計算する。step が偶数の場合は 〇 の 手番
            self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
            # status 属性を judget を使って計算する
            self.status = self.judge()
            # 描画を更新する
            self.draw_board(ax, ai)
        
        def on_first_button_clicked(b):
            change_step(0)

        first_button.on_click(on_first_button_clicked)

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    else:
        ax = None

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        def change_step(step):
            # step 手目のゲーム盤のデータをコピーし、board に代入する
            self.board = deepcopy(self.board_records[step])
            # 手番を計算する。step が偶数の場合は 〇 の 手番
+           self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
            # status 属性を judget を使って計算する
+           self.status = self.judge()
            # 描画を更新する
            self.draw_board(ax, ai)
元と同じなので省略

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

__実行結果__ は __省略__ しますが、__上記__ の __修正後__ に、__下記__ のプログラムで __`gui_play`__ を __実行__ し、__1 つ目__ と __2 つ目__ の __問題__ が __解決された__ ことを __確認__ して下さい。

```python
gui_play()
```

### 3 つ目の問題の原因の検証

さまざまな __試行錯誤__ を __繰り返す__ ことで __わかるようになる__ と思いますが、__この現象__ は __<<__ ボタンを __クリック__ して __ゲーム開始時__ の __局面__ に __戻した後__ で __発生__ する現象です。__リセット__ ボタンを __クリック__ してゲームをリセットした __場合__ には __発生しません__。

この問題は、__引き分け__ に __関する現象__ なので、__引き分け__ の __判定方法__ を __調べる必要__ が __あります__。__引き分け__ の __判定__ は、__`judge`__ メソッド内の __下記__ のプログラムで行っています。

```python
  elif self.is_full():
      return Marubatsu.DRAW
```

__上記__ の __`is_full`__ は __下記__ のように __定義__ される __メソッド__ です。

```python
    def is_full(self):
        return self.move_count == self.BOARD_SIZE ** 2
```

__上記__ から、__着手__ した __回数__ を表す __`move_count`__ 属性が、__ゲーム盤__ の __マスの数__ に __等しい場合__ に __引き分け__ と __判定__ されることがわかります。そこで、__`move_count`__ に __関する処理__ を __調べる__ と、__`move_count`__ に対する __処理__ は __以下の場合__ に __行われる__ ことが __わかります__。

* __`restart`__ メソッドで __ゲーム__ を __リセットした際__ に、__`0` で初期化__ される
* __`move`__ メソッドで __着手__ が __行われた際__ に __`1` 加算__ される

従って、__<<__ ボタンで __ゲーム開始時__ の __局面__ を __表示__ した場合は、__`move_count`__ 属性の __値__ は __変化しません__。例えば、__6 手目__ まで __着手__ を __行った場合__ に __<<__ ボタンを __クリック__ すると、__ゲーム盤の表示__ は __ゲーム開始時__ の __表示__ に __戻ります__ が、__着手__ を __行った回数__ を表す __`move_count`__ 属性の __値__ は __`6` のまま__ です。そのため、__その後__ で __3 回着手__ を行うと、__`move_count`__ の値が __`9`__ になり、__下図__ のように __引き分け__ になってしまいます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/6b2a5bff-dbf8-908d-bee9-774a8c2d3c8e.png" width="250">

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

### 3 つ目の問題の修正

この問題は、__<<__ ボタンを __クリック__ した時に、 __`move_count`__ に __`0`__ を __代入する__ ことで __解決__ できます。ただし、__`change_step`__ は 0 手目だけでなく、__任意の手数__ に移動する処理を行うので、__`move_count`__ には __`0`__ ではなく、__下記__ のプログラムの __6 行目__ のように、__手数__ を表す __仮引数 `step`__ をそのまま __代入__ することで __この問題__ を __解決__ することが __できます__。

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          def change_step(step):
 3              # step 手目のゲーム盤のデータをコピーし、board に代入する
 4              self.board = deepcopy(self.board_records[step])
 5              # 手数を表す step を move_count に代入する
 6              self.move_count = step
 7              # 手番を計算する。step が偶数の場合は 〇 の 手番
 8              self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
 9              # status 属性を judget を使って計算する
10              self.status = self.judge()
11              # 描画を更新する
12              self.draw_board(ax, ai)       
元と同じなので省略
13
14  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "〇" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step 手目のゲーム盤のデータをコピーし、board に代入する
            self.board = deepcopy(self.board_records[step])
            # 手数を表す step を move_count に代入する
            self.move_count = step
            # 手番を計算する。step が偶数の場合は 〇 の 手番
            self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
            # status 属性を judget を使って計算する
            self.status = self.judge()
            # 描画を更新する
            self.draw_board(ax, ai)
        
        def on_first_button_clicked(b):
            change_step(0)

        first_button.on_click(on_first_button_clicked)

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    else:
        ax = None

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        def change_step(step):
            # step 手目のゲーム盤のデータをコピーし、board に代入する
            self.board = deepcopy(self.board_records[step])
            # 手数を表す step を move_count に代入する
+           self.move_count = step
            # 手番を計算する。step が偶数の場合は 〇 の 手番
            self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
            # status 属性を judget を使って計算する
            self.status = self.judge()
            # 描画を更新する
            self.draw_board(ax, ai)       
元と同じなので省略

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

__実行結果__ は __省略__ しますが、__上記__ の __修正後__ に、__下記__ のプログラムで __`gui_play`__ を __実行__ し、__1 つ目__ と __2 つ目__ の __問題__ が __解決された__ ことを __確認__ して下さい。

```python
gui_play()
```

## 残りボタンのイベントハンドラの定義

__1 手戻す__ 処理と __1 手進む__ 処理は、__現在表示__ されている __局面の手数__ の __前後__ の __手数の局面__ を __表示__ するという __処理__ です。__現在の手数__ は __`move_count`__ に __代入__ されているので、__その値__ を __元__ に __`change_step`__ メソッドを使って __イベントハンドラ__ を __記述__ することが __できます__。

__最後__ に __行われた着手__ まで __進める__ という __処理__ は、慣れないとどのように記述すれば良いかわからないかもしれません。__どこか__ に __最後__ に __行われた着手__ が __何手目__ であるかを __表すデータ__ が __存在している__ ので、__それが何か__ を少し考えてみて下さい。

__`board_records`__ には __ゲーム開始時__ から、__最後__ に __行われた着手__ の局面までの __ゲーム盤のデータ__ が __list の要素__ に __代入__ されています。従って、__list__ の __要素数__ を __計算__ する __組み込み関数 `len`__ を使って、__`board_records`__ の __要素の数__ から、__最後__ に __行われた着手__ の __手数__ を __計算__ することが __できます__。__その際__ に、`__board_records`__ には __着手__ を __行う前__ の __ゲーム開始時__ の __局面のデータ__ が __代入__ されているため、__`len(board_records) - 1`__ のように、__要素の数__ から __1 を引く必要__ がある点に __注意__ して下さい。

下記は、__上記__ の __イベントハンドラ__ と、__ボタン__ に __イベントハンドラ__ を __結び付ける__ 処理を __`play`__ メソッドに __記述__ したプログラムです。

* __5、6 行目__：__1 手前__ の __手数__ を __`self.move_count - 1`__ で __計算__ して __表示__ する
* __8、9 行目__：__1 手後__ の __手数__ を __`self.move_count + 1`__ で __計算__ して __表示__ する
* __11、12 行目__：__最後__ に __行われた着手__ の __手数__ を __`len(self.board_records) - 1`__ で __計算__ して __表示__ する
* __15 ~ 17 行目__：__それぞれ__ の __ボタン__ と __イベントハンドラ__ を __結び付ける__

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          def on_first_button_clicked(b):
 3              change_step(0)
 4
 5          def on_prev_button_clicked(b):
 6              change_step(self.move_count - 1)
 7
 8          def on_next_button_clicked(b):
 9              change_step(self.move_count + 1)
10            
11          def on_last_button_clicked(b):
12              change_step(len(self.board_records) - 1)
13
14          first_button.on_click(on_first_button_clicked)
15          prev_button.on_click(on_prev_button_clicked)
16          next_button.on_click(on_next_button_clicked)
17          last_button.on_click(on_last_button_clicked)
元と同じなので省略
18
19  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "〇" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step 手目のゲーム盤のデータをコピーし、board に代入する
            self.board = deepcopy(self.board_records[step])
            # 手数を表す step を move_count に代入する
            self.move_count = step
            # 手番を計算する。step が偶数の場合は 〇 の 手番
            self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
            # status 属性を judget を使って計算する
            self.status = self.judge()
            # 描画を更新する
            self.draw_board(ax, ai)
        
        def on_first_button_clicked(b):
            change_step(0)

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

        def on_next_button_clicked(b):
            change_step(self.move_count + 1)
            
        def on_last_button_clicked(b):
            change_step(len(self.board_records) - 1)

        first_button.on_click(on_first_button_clicked)
        prev_button.on_click(on_prev_button_clicked)
        next_button.on_click(on_next_button_clicked)
        last_button.on_click(on_last_button_clicked)

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    else:
        ax = None

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        def on_first_button_clicked(b):
            change_step(0)

+       def on_prev_button_clicked(b):
+           change_step(self.move_count - 1)

+       def on_next_button_clicked(b):
+           change_step(self.move_count + 1)
            
+       def on_last_button_clicked(b):
+           change_step(len(self.board_records) - 1)

        first_button.on_click(on_first_button_clicked)
+       prev_button.on_click(on_prev_button_clicked)
+       next_button.on_click(on_next_button_clicked)
+       last_button.on_click(on_last_button_clicked)
元と同じなので省略

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

__実行結果__ は __省略__ しますが、__上記__ の __修正後__ に、__下記__ のプログラムで __`gui_play`__ を __実行__ し、4 つのボタンでリプレイを行うことができることを __確認__ して下さい。

```python
gui_play()
```

# 今回の記事のまとめ

今回の記事では、__リプレイ機能__ を行うための __4 つ__ の __ボタンの配置__ と、__イベントハンドラ__ の __定義__ を行いました。__それぞれ__ の __ボタン__ で __実際__ に __リプレイ__ を行うことが __できます__ が、__4 つのボタン__ に対して __様々な操作__ を __行う__ ことで、__4 つのボタン__ が __行う処理__ には __いくつかの問題がある__ ことが __わかる__ と思います。__次回の記事__ では __それらの問題__ を __解決する方法__ を __紹介__ するので、余裕がある方は __どのような問題があるか__ について __調べておいてください__。

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

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

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

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

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

# 次回の記事

https://qiita.com/ysgeso/items/94fba56cb97dc4787ca8