Pythonで〇×ゲームのAIを一から作成する　その83　GUIの機能の分離の完了と差分による局面の再現

# 目次と前回の記事

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

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

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

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

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

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

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

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

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

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



# 前回の記事で発生しなかったエラー

前回の記事では、__`Marubatsu_GUI`__ クラスを定義 することで __`Marubatsu` クラスから GUI の機能を分離する作業__ の中で、__ゲーム盤の描画を行う処理を実装__ しました。

実は、前回の記事で記述したプログラムにはバグがあるため、下記のプログラムで __`gui_play`__ を実行すると、前回の記事で `gui_play` を実行した際に発生しなかった、実行結果のようなエラーが発生します。このエラーの原因について少し考えてみて下さい。

```python
from util import gui_play

gui_play()
```

実行結果（ボタンやゲーム盤の画像は省略します）

```
略
File c:\Users\ys\ai\marubatsu\083\marubatsu.py:665, in Marubatsu_GUI.update_widgets_status(self)
    662 """ウィジェットの状態を更新する."""
    664 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
--> 665 set_button_status(self.first_button, self.mb.move_count <= 0)
    666 set_button_status(self.prev_button, self.mb.move_count <= 0)
    667 set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)

NameError: name 'set_button_status' is not defined
```

## エラーの検証と修正

エラーメッセージから、__`update_widgets_status`__ の中で、__定義されていない `set_button_status` を呼び出そうした__ ためエラーが発生したことがわかります。

この、__`set_button_status`__ は、[前回の記事](https://qiita.com/ysgeso/items/c2d569b1be171fbe4b31#set_button_status-%E3%81%A8-update_widgets_status-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%AE%9A%E7%BE%A9)で __`Marubatsu_GUI` のメソッドとして定義__[^1]したものなので、呼び出す際には `self.set_button_status` のように記述する必要があります。従って、下記のプログラムのように `update_widgets_status` を修正することでこの問題を解決することができます。

* __5 ~ 8 行目__：`set_button_status` を `self.set_button_status` に修正する

```python
 1  from marubatsu import Marubatsu_GUI
 2  
 3  def update_widgets_status(self):   
 4      # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
 5      self.set_button_status(self.first_button, self.mb.move_count <= 0)
 6      self.set_button_status(self.prev_button, self.mb.move_count <= 0)
 7      self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
 8      self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)          
 9
10  Marubatsu_GUI.update_widgets_status = update_widgets_status
```

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

```python
from marubatsu import Marubatsu_GUI

def update_widgets_status(self):   
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    self.set_button_status(self.first_button, self.mb.move_count <= 0)
    self.set_button_status(self.prev_button, self.mb.move_count <= 0)
    self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
    self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)          

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

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

```diff_python
from marubatsu import Marubatsu_GUI

def update_widgets_status(self):   
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
-   set_button_status(self.first_button, self.mb.move_count <= 0)
+   self.set_button_status(self.first_button, self.mb.move_count <= 0)
-   set_button_status(self.prev_button, self.mb.move_count <= 0)
+   self.set_button_status(self.prev_button, self.mb.move_count <= 0)
-   set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
+   self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
-   set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)          
+   self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)          

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

実行結果は省略しますが、上記の修正後に、下記のプログラムで `gui_play` を実行してエラーが発生しなくなることを確認して下さい。

```python
gui_play()
```

## 前回の記事でエラーが発生しなかった理由

__[前回の記事](https://qiita.com/ysgeso/items/c2d569b1be171fbe4b31#%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%AE%E6%A4%9C%E8%A8%BC%E3%81%A8%E4%BF%AE%E6%AD%A3-3)でこのエラーが発生しなかった点__ について疑問に思っている人が多いかもしれません。何故、前回の記事でこのエラーが発生しなかったかについて少し考えてみて下さい。

前回の記事では、下記のプログラムで `set_button_status` メソッドを定義しました。

```python
@staticmethod
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
    button.disabled = disabled
    button.style.button_color = "lightgray" if disabled else "lightgreen"

Marubatsu_GUI.set_button_status = set_button_status
```

このプログラムでは、__通常の関数として `set_button_status` を定義した後__ で、__`Marubatsu_GUI` の属性にこの関数を代入__ するという形で `set_button_status` メソッドを定義しました。そのため、__上記のプログラムを実行した後__ では、__`set_button_status` を通常の関数として呼び出すことが可能__ です。これが、前回の記事で `update_widgets_status` メソッドの中で `set_button_status` を呼び出してもエラーが発生しなかった原因です。

一方、今回の記事では、VSCode で新しい JupyterLab のファイルを作成したので、そのファイルでプログラムを実行した際には、上記の __`set_button_status` という関数は定義されていません__。また、__marubatsu.py__ の中で、`set_button_status` は、__`Marubatsu_GUI` クラスの定義の中でメソッドとして定義__ されていますが、__通常の関数としては定義されていない__ ので、先程のようなエラーが発生します。

:::note
これまでの記事で「関数を定義し、その関数をクラスの属性に代入する」という方法でクラスのメソッドの定義を修正してきたのは、修正するメソッドの定義だけを JupyterLab のセルに記述して実行することで、メソッドを修正できるからです。

この方法は個別のメソッドを修正する際は便利ですが、定義した関数を通常の関数として利用できてしまうという点に注意が必要です。

なお、修正したメソッドを marubatsu.py に反映させる際には、クラスの定義の中に修正したメソッドを記述したほうが良いので、そのようにしています。
:::

# `draw_mark` の定義の移動

前回の記事では、GUI に関する機能を `Marubatsu` クラスから分離するために、__ゲーム盤を描画__ する処理を行う __`draw_board` メソッドの定義__ を、`Marubatsu` クラスから __`Marubatsu_GUI` クラスのメソッドに移動__ しました。筆者は、これで GUI に関する機能を完全に分離できたと勘違いしていたのですが、`Marubatsu` クラスの定義をよく見ると、__ゲーム盤にマークを描画__ する __`draw_mark`__ というメソッドが __まだ残っている__ ことに気づきましたので、この関数も `Marubatsu_GUI` クラスに移動することにします。

__`draw_mark`__ は __静的メソッド__ として定義したので、前回の記事で __様々なバグの原因となった `self` は存在しません__。そのため、下記のプログラムのように、単純に __その定義をコピーするだけ__ で `Marubatsu_GUI` クラスのメソッドとして定義しなおすことができます。

下記の `draw_mark` の定義は `Marubatsu` クラスの `draw_mark` と全く同じです。

```python
from marubatsu import Marubatsu
import matplotlib.patches as patches

@staticmethod
def draw_mark(ax, x, y, mark, color="black"):
    if mark == Marubatsu.CIRCLE:
        circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
        ax.add_artist(circle)
    elif mark == Marubatsu.CROSS:
        ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
        ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")

Marubatsu_GUI.draw_mark = draw_mark
```

次に、__`draw_mark` は `draw_board` メソッドの中から呼び出されている__ ので、下記のプログラムのように、`draw_board` を修正する必要があります。

* __6 行目__：`self.mb.draw_mark` を `self.draw_mark` に修正することで、`Marubatsu_GUI` クラスの `draw_mark` メソッドを呼び出すように修正する

```python
 1  def draw_board(self):
元と同じなので省略
 2      # ゲーム盤のマークを描画する
 3      for y in range(self.mb.BOARD_SIZE):
 4          for x in range(self.mb.BOARD_SIZE):
 5              color = "red" if (x, y) == self.mb.last_move else "black"
 6              self.draw_mark(ax, x, y, self.mb.board[x][y], color)
 7
 8      self.update_widgets_status() 
 9    
10  Marubatsu_GUI.draw_board = draw_board  
```

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

```python
def draw_board(self):
    ax = self.ax
    ai = self.mb.ai
    
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "lightyellow"
    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]}　VS　{names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

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

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

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

```diff_python
def draw_board(self):
元と同じなので省略
    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
-           self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)            
+           self.draw_mark(ax, x, y, self.mb.board[x][y], color)            

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

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

```python
gui_play()
```

# イベントハンドラに関する関数の定義

次は、__`play`__ メソッドに元々記述されていた、__イベントハンドラを定義し、ボタンなどに結びつける処理__ を実装します。イベントハンドラも `Marubatsu_GUI` クラスのメソッドとして定義すればよいのではないかと思う人がいるかもしれませんが、__イベントハンドラ__ は、`Marubatsu_GUI` クラスのインスタンスからではなく、__ipywidgets のイベントループの中から呼び出される__ ので、__イベントハンドラに実引数で `Marubatsu_GUI` クラスのインスタンスの情報を渡すことはできません__。そのため、イベントハンドラの中で `Marubatsu_GUI` の情報を利用するためには、`play` メソッドで行っていたのと同様に、`Marubatsu_GUI` クラスのメソッドの __ローカル関数としてイベントハンドラを定義__ する必要があります。

## `create_event_handler` のひな形

そこで、イベントハンドラをローカル関数として定義し、ウィジェットに結びつける処理を行う __`create_event_handler`__ というメソッド定義することにします。

前回の記事で、`play` メソッドの中から __イベントハンドラに関する処理を削除__ してしまったので、[前々回の記事の `play` メソッド](https://github.com/ysgeso/marubatsu/blob/master/082/marubatsu.py#L265)の中から、__イベントハンドラに関する処理を抜き出してコピー__ することで、下記のプログラムのように `create_event_handler` を定義し、__これを雛形に `create_event_handler` が正しく動作するように修正__ していくことにします。

```python
import math
from copy import deepcopy

def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    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)   
    
    def change_step(step):
        # step の範囲を正しい範囲に修正する
        step = max(0, min(len(self.board_records) - 1, 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.last_move = self.records[step]
        # 描画を更新する
        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)

    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    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)    
               
Marubatsu_GUI.create_event_handler = create_event_handler
```

## `Marubatsu` クラスでの `change_step` の定義

`create_event_handler` の中で定義されている __`change_step`__ が行う処理のうち、ゲーム盤の描画を更新する `self.draw_board` __以外の処理__ は、`step` 手目の局面に移動するという処理であり、__GUI の処理ではありません__。また、__特定の手数の局面に移動__ するという処理は、__GUI 以外の場合でも利用できた方が便利__ ですが、この関数を `create_event_handler` の中の __ローカル関数として定義すると GUI でしか利用できない__ という欠点が生じます。

そこで、__`change_step`__ の中の `self.draw_board` __以外の処理を行うメソッド__ を 下記のプログラムのように、__`Marubatsu` クラスに同じ名前と仮引数で定義__ して移動することにします。このようにすることで、__`Marubatsu` クラスのインスタンス__ から、__`change_step`__ を呼び出して、__任意の手数の局面に移動できる__ ようになります。

* __1 行目__：ローカル関数をメソッドとして定義し直すので、仮引数 `self` を追加する
* 4 行目の下にあった描画を更新する処理を削除する

```python
1  def change_step(self, step):
元と同じなので省略
2      # 直前の着手を計算する
3      self.last_move = self.records[step]
4      # この下にあった描画を更新する処理を削除する
5
6  Marubatsu.change_step = change_step     
```

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

```python
def change_step(self, step):
    # step の範囲を正しい範囲に修正する
    step = max(0, min(len(self.board_records) - 1, 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.last_move = self.records[step]

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

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

```diff_python
-def change_step(step):
+def change_step(self, step):
    # step の範囲を正しい範囲に修正する
    step = max(0, min(len(self.board_records) - 1, 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.last_move = self.records[step]
    # 描画を更新する
-   self.draw_board(ax, ai)  

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

次に、`create_event_handler` のローカル関数 `change_step` を下記のプログラムのように修正します。

* __3 行目__：手数を移動する処理を削除し、代わりに先程定義した `Marubatsu` クラスの `change_step` を呼び出す
* __5 行目__：描画を更新する処理は GUI の処理なので元のまま残す

```python
1  def create_event_handler(self):
元と同じなので省略
2      def change_step(step):
3          self.mb.change_step(step)
4          # 描画を更新する
5          self.draw_board(ax, ai)        
元と同じなので省略
6               
7  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

```python
def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            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)   
    
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        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)

    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    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)    
               
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

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

```diff_python
def create_event_handler(self):
元と同じなので省略
    def change_step(step):
        # step の範囲を正しい範囲に修正する
-       step = max(0, min(len(self.board_records) - 1, 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.last_move = self.records[step]
+       self.mb.change_step(step)
        # 描画を更新する
        self.draw_board(ax, ai)        
元と同じなので省略
               
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

なお、このローカル関数 `change_step` を `Marubatsu_GUI` クラスのメソッドとして定義する事もできますが、本記事では以下の理由からローカル関数として定義する事にします。

* ローカル関数 `change_step` は、`create_event_handler` の中で定義されたイベントハンドラの中からしか呼び出されない
* `change_step` を `Marubatsu_GUI` のメソッドとして定義すると、__`self.change_step` と記述した場合__ に、`Marubatsu` クラスと `Marubatsu_GUI` クラスの __どちらの `change_step` メソッドを呼び出しているかがわかりにくくなる__。なお、この問題を解決する別の方法として、`change_step` とは別の名前を付けるという方法も考えられる

## `create_event_handler` のブロックの中の名前の修正

上記の `create_event_handler` は、VSCode では下図のように多くの部分に、__名前が定義されていないことを表すオレンジ色の波線が表示__ されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/2a3b9ea6-2cfa-4ef4-4106-20ad9278e593.png" width="500">

これらの波線は、前回の記事で、`play` メソッドの中でローカル変数だったものが、`Marubatsu_GUI` の属性に変更されたことなどが原因で __定義されなくなった名前__ を表します。

また、`play_loop` などの __仮引数が変更されたメソッド__ などや、前回の記事で多くのバグの原因となった __`self` の意味の混同に関する修正__ を行う必要があります。

下記は修正を行う必要がある名前の一覧です。本記事では、これらをすべて修正したプログラムを下記に示しますが、このような複雑で大量の修正を行う必要がある場合は、__修正漏れが発生し、プログラムを実行するとエラーが発生する可能性が高い__ でしょう。そのような場合は、前回の記事で行ったように、__エラーが発生するたびに原因を検証して修正するという、地道な作業を行う__ 必要があります。

### `Marubatsu` クラスの属性になった名前

下記は、先頭に `self.mb.` をつけるという修正を行う必要があります。

|  | 行数 |
|:--|--:|
| `ai`  | 5  |
| `self.play_loop` | 6、48 |
| `self.restart`  | 10  |
| `move_count`  | 26、29 |
| `board_records`  | 32  |
| `status`  |  42 |
| `move`  |  45 |

### `Marubatsu_GUI` クラスの属性になった名前

下記は、先頭に `self.` をつけるという修正を行う必要があります。

|  | 行数 |
|:--|--:|
| `dropdown_list`  | 5  |
| `change_button`  | 14  |
| `reset_button`  |  15 |
| `fig`  | 51  |

* __仮引数が変化したメソッド__

|  | 変更点 | 行数 |
|:--|:--|--:|
| `play_loop`  | 仮引数が無くなった  | 6、48  |
| `draw_board`  | 仮引数が無くなった | 20、46  |

### 変更してはいけない名前

下記の名前は `Marubatsu_GUI` の属性なので、変更してはいけない点に注意して下さい。
    * `self.draw_board`
    * `self.first_button`
    * `self.prev_button`
    * `self.next_button`
    * `self.last_button`

なお、上記の修正は、下記の理由から前回の記事で紹介した[シンボル名の変更](https://qiita.com/ysgeso/items/c2d569b1be171fbe4b31#vscode-%E3%81%AE%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AB%E5%90%8D%E3%81%AE%E5%A4%89%E6%9B%B4%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)の機能を利用することはできないので、検索や置換の機能を使って地道に行う必要があります。

* `ai` などは、名前が定義されていないので __[文法的に正しくない](https://qiita.com/ysgeso/items/c2d569b1be171fbe4b31#%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AB%E5%90%8D%E3%81%AE%E5%A4%89%E6%9B%B4%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9)__
* __異なる意味の `self` が混在する__ ので、シンボル名の変更を行うと、変更してはいけない `self` までもが変更されてしまう

### 修正した `create_event_handler` の定義

下記は、上記の修正を行った `create_event_handler` の定義を行うプログラムです。

```python
 1  def create_event_handler(self):
 2      # 変更ボタンのイベントハンドラを定義する
 3      def on_change_button_clicked(b):
 4          for i in range(2):
 5              self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
 6          self.mb.play_loop()
 7
 8      # リセットボタンのイベントハンドラを定義する
 9      def on_reset_button_clicked(b):
10          self.mb.restart()
11          on_change_button_clicked(b)
12        
13      # イベントハンドラをボタンに結びつける
14      self.change_button.on_click(on_change_button_clicked)
15      self.reset_button.on_click(on_reset_button_clicked)   
16    
17      def change_step(step):
18          self.mb.change_step(step)
19          # 描画を更新する
20          self.draw_board() 
21
22      def on_first_button_clicked(b):
23          change_step(0)
24
25      def on_prev_button_clicked(b):
26          change_step(self.mb.move_count - 1)
27
28      def on_next_button_clicked(b):
29          change_step(self.mb.move_count + 1)
30        
31      def on_last_button_clicked(b):
32          change_step(len(self.mb.board_records) - 1)
33
34      self.first_button.on_click(on_first_button_clicked)
35      self.prev_button.on_click(on_prev_button_clicked)
36      self.next_button.on_click(on_next_button_clicked)
37      self.last_button.on_click(on_last_button_clicked)
38    
39      # ゲーム盤の上でマウスを押した場合のイベントハンドラ
40      def on_mouse_down(event):
41          # Axes の上でマウスを押していた場合のみ処理を行う
42          if event.inaxes and self.mb.status == Marubatsu.PLAYING:
43              x = math.floor(event.xdata)
44              y = math.floor(event.ydata)
45              self.mb.move(x, y)                
46              self.draw_board()
47              # 次の手番の処理を行うメソッドを呼び出す
48              self.mb.play_loop()
49            
50      # fig の画像にマウスを押した際のイベントハンドラを結び付ける
51      self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    
52               
53  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

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

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b):
        self.mb.restart()
        on_change_button_clicked(b)
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b):
        change_step(0)

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

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

    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop()
            
    # fig の画像にマウスを押した際のイベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    
               
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

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

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

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b):
-       self.restart()
+       self.mb.restart()
        on_change_button_clicked(b)
        
    # イベントハンドラをボタンに結びつける
-   change_button.on_click(on_change_button_clicked)
+   self.change_button.on_click(on_change_button_clicked)
-   reset_button.on_click(on_reset_button_clicked)   
+   self.reset_button.on_click(on_reset_button_clicked)   
    
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
-       self.draw_board(ai, ax)        
+       self.draw_board()        

    def on_first_button_clicked(b):
        change_step(0)

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

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

    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
-       if event.inaxes and self.status == Marubatsu.PLAYING:
+       if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
-           self.move(x, y)                
+           self.mb.move(x, y)                
-           self.draw_board(ai, ax)
+           self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
-           self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
+           self.mb.play_loop()
            
    # fig の画像にマウスを押した際のイベントハンドラを結び付ける
-   fig.canvas.mpl_connect("button_press_event", on_mouse_down)    
+   self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    
               
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

:::note warn
このように、バグの修正はかなり地道な作業が必要になりますが、避けて通ることはできません。個人的には、__プログラミングで重要となる能力の一つ__ が、何度も何度も発生するバグを修正する __忍耐力__ ではないかと思っています。
:::

次に、下記のプログラムの 3 行目のように、`Marubatsu_GUI` の `__init__` 内で、`create_event_handler` を呼び出すように修正します。

```python
1  def __init__(self, mb, ai_dict=None, size=3):
元と同じなので省略
2      self.create_widgets()
3      self.create_event_handler()
4      self.display_widgets() 
5        
6  Marubatsu_GUI.__init__ = __init__

```

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

```python
def __init__(self, mb, ai_dict=None, size=3):
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}

    self.mb = mb
    self.ai_dict = ai_dict
    self.size = size
    
    # %matplotlib widget のマジックコマンドを実行する
    get_ipython().run_line_magic('matplotlib', 'widget')
        
    self.create_widgets()
    self.create_event_handler()
    self.display_widgets() 
        
Marubatsu_GUI.__init__ = __init__
```
</details>

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

```diff_python
def __init__(self, mb, ai_dict=None, size=3):
元と同じなので省略
    self.create_widgets()
    self.create_event_handler()
    self.display_widgets() 
        
Marubatsu_GUI.__init__ = __init__
```
</details>

実行結果は省略しますが、上記の修正後に、下記のプログラムで、`gui_play` を実行すると、ゲーム盤のクリックによる着手や、リセットボタンやリプレイボタンなどが正しく動作するようになることが確認できます。

```python
gui_play()
```

## AI が手番を担当した場合のエラーと修正

上記では、人間どうしが対戦した場合の確認を行いましたが、Dropdown で AI を選択して AI どうしが対戦をおこなうようにしてからリセットボタンをクリックすると、下記のようなエラーが発生します。このエラーに関しては、これまでの知識では理解できないと思います。

```
略
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\site-packages\ipywidgets\widgets\widget.py:512, in Widget.__deepcopy__(self, memo)
    511 def __deepcopy__(self, memo):
--> 512     raise NotImplementedError("Widgets cannot be copied; custom implementation required")

NotImplementedError: Widgets cannot be copied; custom implementation required
```

:::note
上記のエラーメッセージは、以下のような意味を持ちます。

* __NotImplementedError__
実装（Implemented）されていない（Not）ことを表すエラー
* __Widgets cannot be copied; custom implementation required__
ウィジェット（Widgets）はコピーできない（can not be copied）。（ウィジェットをコピーするためには）独自の（custon）実装（implementation）が必要である（required）
:::

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

エラーメッセージから、__ウィジェットをコピーしようとして発生したエラー__ であることがわかります。そこで、__どこでウィジェットがコピーされたかを検証__ することにします。

このエラーは、人間どうしの対戦では発生しなかったので、__AI が着手を選択する処理の中で発生している可能性が高い__ と推測できます。先ほどの __エラーメッセージをさかのぼってみてみる__ と、下記のように、AI が着手を選択する際に実行する __`ai_by_score`__ の中で、__`deepcopy(mb_orig)` を呼び出した際にこのエラーが発生する__ ことがわかります。

```
File c:\Users\ys\ai\marubatsu\083\ai.py:130, in ai_by_score(mb_orig, eval_func, debug, rand)
    128 dprint(debug, "=" * 20)
    129 dprint(debug, "move", move)
--> 130 mb = deepcopy(mb_orig)
    131 x, y = move
    132 mb.move(x, y)
```

__`deepcopy`__ は[以前の記事](https://qiita.com/ysgeso/items/ff28a95dff50322e6b16#%E6%B7%B1%E3%81%84%E3%82%B3%E3%83%94%E3%83%BC)で説明したように、__深いコピー__ を行う組み込み関数です。深いコピーは、__オブジェクトから辿ることができるすべての属性をコピーする処理__ が行われるので、この部分では下記のような処理が行われます。

* `mb_orig` は `Marubatsu` クラスのインスタンスなので、__`Marubatsu` クラスのインスタンスの深いコピー__ の処理が行われる
* `deepcopy` は、`mg_orig` の __すべての属性をコピー__ するという処理を行う
* `Marubatsu` クラスのインスタンスには、__`mb_gui`__ という、__`Marubatsu_GUI` クラスのインスタンスが代入された属性__ が存在する
* `deepcopy` は __`mb_gui` 属性をコピー__ する際に、`mb_gui` に代入された __`Marubatsu_GUI`__ クラスのインスタンスの __すべての属性をコピー__ するという処理を行う
* __`mb_gui` 属性__ には、__`first_button` など__ の、__ウィジェットが代入された属性が存在する__

上記から、__`deepcopy(mb_orig)`__ によって、__ウィジェットをコピーする処理が行われる__ ことが確認できました。

### エラーの修正

これまでの記事では説明していませんでしたが、__`deepcopy` には__、ウィジェットなど、__コピーすることができないオブジェクト__ があり、そのようなオブジェクトをコピーしようとすると、先程のような __`NotImplementedError` というエラーが発生__ します。従って、このエラーを修正するためには、__`Marubatsu` クラスのインスタンスから辿れるの属性の中__ に、__ウィジェットのデータが存在しないようにする__ 必要があります。

__ウィジェットのデータが代入__ されているのは、__`Marubatsu_GUI`__ クラスのインスタンスが代入されている __`mb_gui` 属性__ です。__`Marubatsu_GUI` クラス__ は 〇×ゲームを __GUI で遊ぶために必要な処理を行う__ ものですが、__AI が着手を選択する際に GUI の機能は必要がありません__。そのため、__`Marubatsu`__ クラスのインスタンスに __`mb_gui` 属性が存在しなくても AI が着手を選択することができます__。そこで、__`Marubatsu`__ クラスのインスタンスから __`mg_gui` 属性を削除する__ ことで、上記の問題を解決することにします。

具体的には、下記のようにプログラムを修正します。

* __`play`__ メソッド内で作成する __`Marubatsu_GUI`__ クラスのインスタンスを __ローカル変数 `mb_gui` に代入__ する
* __`play_loop` 内__ の __`self.mb_gui` が利用できなくなる__ ので、その代わりに `Marubatsu_GUI` クラスのインスタンスを代入する __仮引数 `mb_gui` を `play_loop` に追加する__
* __`play_loop`__ を呼び出す処理の __実引数に `Marubatsu_GUI` クラスのインスタンスを記述__ する

:::note warn
__`deepcopy` がコピーするデータは__ 、オブジェクトから辿れる __属性の値だけ__ で、__ローカル変数の値はコピーしません__。従って、上記の修正を行うことで、`deepcopy` がウィジェットのデータをコピーしなくなるため、エラーが発生しなくなります。
:::

下記は、`play` メソッドを修正したプログラムです。

* __4 行目__：`Marubatsu_GUI` のインスタンスをローカル変数 `mb_gui` に代入するようにする
* __7 行目__：`play_loop` の実引数に `mb_gui` を追加する

```python
1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
2      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
3      if gui:
4          mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
5        
6      self.restart()
7      return self.play_loop(mb_gui)
8
9  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):   
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
        
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
    self.params = params
    self.verbose = verbose
    self.gui = gui

    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
        
    self.restart()
    return self.play_loop(mb_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):   
元と同じなので省略
    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
-       self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
+       mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
        
    self.restart()
-   return self.play_loop()
+   return self.play_loop(mb_gui)

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

:::note
上記の 4 行目の修正は、__[前回の記事](https://qiita.com/ysgeso/items/c2d569b1be171fbe4b31#%E6%96%B0%E3%81%9F%E3%81%AA%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%AE%E6%A4%9C%E8%A8%BC%E3%81%A8%E4%BF%AE%E6%AD%A3-1)で行った修正を元に戻す__ という修正でなので、__前回の記事の修正は無駄だったのではないか__ と思った人がいるかもしれません。

確かに、__結果として__ は前回の記事の修正は __無駄のように見えるかもしれません__ が、__前回の記事の時点__ では __意味のある修正__ であり、今回の記事で前回とは __事情が変化したため__ 元に戻したので、__一概には無駄であると言えません__。

プログラムでは、それほど頻繁ではありませんが、__一度何らかの理由で修正した内容__ を、__後から別の理由で元に戻す__ ということが実際行われることがあります。
:::

下記は、`play_loop` メソッドを修正したプログラムです。

* __1 行目__：仮引数 `mb_gui` を追加する
* __5、9 行目__：`self.mb_gui` を `mb_gui` に修正する

```python
 1  def play_loop(self, mb_gui):
元と同じなので省略
 2             if gui:
 3                  # AI どうしの対戦の場合は画面を描画しない
 4                  if ai[0] is None or ai[1] is None:
 5                      mb_gui.draw_board()
元と同じなので省略
 6      # 決着がついたので、ゲーム盤を表示する
 7      if verbose:
 8          if gui:
 9              mb_gui.draw_board()
10          else:
11              print(self)
12            
13      return self.status
14
15  Marubatsu.play_loop = play_loop
```

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

```python
def play_loop(self, mb_gui):   
    ai = self.ai
    params = self.params
    verbose = self.verbose
    gui = self.gui
    
    # ゲームの決着がついていない間繰り返す
    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:
                    mb_gui.draw_board()
                # 手番を人間が担当する場合は、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:
            mb_gui.draw_board()
        else:
            print(self)
            
    return self.status

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

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

```diff_python
-def play_loop(self):
+def play_loop(self, mb_gui):
元と同じなので省略
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
-                   self.mb_gui.draw_board()
+                   mb_gui.draw_board()
元と同じなので省略
    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
-           self.mb_gui.draw_board()
+           mb_gui.draw_board()
        else:
            print(self)
            
    return self.status

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

`play_loop` は `create_event_handler` 内で呼び出されているので、その部分を下記のプログラムのように修正します。

* __6、16 行目__：`play_loop` の実引数に、`Marubatsu_GUI` のインスタンスを表す `self` を追加する。紛らわくてわかりづらいが、`play_loop` の仮引数 `self` には `Marubatsu` クラスのインスタンスである `self.mb` が、`play_loop` の仮引数 `mb_gui` には、`Marubatsu_GUI` クラスのインスタンスである `self` が代入される

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      # 変更ボタンのイベントハンドラを定義する
 3      def on_change_button_clicked(b):
 4          for i in range(2):
 5              self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
 6          self.mb.play_loop(self)
元と同じなので省略
 7      # ゲーム盤の上でマウスを押した場合のイベントハンドラ
 8      def on_mouse_down(event):
 9          # Axes の上でマウスを押していた場合のみ処理を行う
10          if event.inaxes and self.mb.status == Marubatsu.PLAYING:
11              x = math.floor(event.xdata)
12              y = math.floor(event.ydata)
13              self.mb.move(x, y)                
14              self.draw_board()
15              # 次の手番の処理を行うメソッドを呼び出す
16              self.mb.play_loop(self)
元と同じなので省略
17               
18  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

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

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b):
        self.mb.restart()
        on_change_button_clicked(b)
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b):
        change_step(0)

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

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

    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)
            
    # fig の画像にマウスを押した際のイベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    
               
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

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

```diff_python
def create_event_handler(self):
元と同じなので省略
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
-       self.mb.play_loop()
+       self.mb.play_loop(self)
元と同じなので省略
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
-           self.mb.play_loop()
+           self.mb.play_loop(self)
元と同じなので省略
               
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

実行結果は省略しますが、上記の修正後に、下記のプログラムで、`gui_play` を実行すると、手番を AI が担当しても、ゲーム盤のクリックによる着手や、リセットボタンやリプレイボタンなどが正しく動作するようになることが確認できます。

```python
gui_play()
```

## CUI での動作確認

GUI の機能を実装するためにさまざまなプログラムの変更を行ってきたので、__CUI でプログラムが正しく動作するか__ を下記のプログラムで確認することにします。実行結果から、エラーが発生することがわかります。このエラーの原因について少し考えてみて下さい。

```python
from ai import ai1s
mb = Marubatsu()

mb.play(ai=[ai1s, ai1s])
```

実行結果

```
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Cell In[17], line 4
      1 from ai import ai1s
      2 mb = Marubatsu()
----> 4 mb.play(ai=[ai1s, ai1s])

Cell In[13], line 17
     14     mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)    
     16 self.restart()
---> 17 return self.play_loop(mb_gui)

UnboundLocalError: cannot access local variable 'mb_gui' where it is not associated with a value
```

### エラーの検証と修正

エラーメッセージから、`play` メソッド内で __`self.play_loop(mb_gui)` を呼び出す際__ に、__`mb_gui` に値が代入されていない__ ことがわかります。`play` メソッドの中で __`mb_gui`__ は __`gui` が `True` の場合だけ値が代入される__ ので、__`gui` が `False` の場合__ に何らかの __値を代入する必要があります__。どのような値を入力すればよいかについて少し考えてみて下さい。

__`mb_gui`__ は `play_loop` の中で、__`gui` が `True` の場合のみ利用されます__。従って、__`gui` が `False` の場合__ は、__`mb_gui` には何が代入されていてもかまわない__ ことがわかります。

そこで、本記事では、下記のプログラムの 5、6 行目のようには、__`gui` が `False` の場合は `mb_gui` に `None` を代入する__ ことにします。

```python
1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):   
元と同じなので省略
2      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
3      if gui:
4          mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)  
5      else:
6          mb_gui = None
元と同じなので省略
7
8  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):   
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
        
    # 一部の仮引数をインスタンスの属性に代入する
    self.ai = ai
    self.params = params
    self.verbose = verbose
    self.gui = gui
    
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)  
    else:
        mb_gui = None
        
    self.restart()
    return self.play_loop(mb_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):   
元と同じなので省略
    # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
    if gui:
        mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)  
    else:
        mb_gui = None
元と同じなので省略

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

:::note
上記のプログラムが行う処理は、GUI の機能を分離する前の [`play` メソッド](https://github.com/ysgeso/marubatsu/blob/master/082/marubatsu.py#L265)で、`gui` が `False` の場合に `ax` に `None` を代入していたのと同様です。
:::

実行結果は省略しますが、下記のプログラムを実行することで、CUI でも正しくプログラムが動作することが確認できます。下記は AI どうしの対戦ですが、人間を含めた CUI の対戦でも正しくプログラムが動作することを確認してみて下さい。

```python
mb.play(ai=[ai1s, ai1s])
```

これでリプレイのために最低限必要な機能は実装できましたが、まだいくつか改良できる部分があるので、その方法を紹介したいと思います。

# 着手の記録による局面の再現

これまでは、__ゲーム盤のデータを記録する方法__ として、[以前の記事](https://qiita.com/ysgeso/items/aa8654c3b5f55a9e08b7#%E3%82%B2%E3%83%BC%E3%83%A0%E7%9B%A4%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E8%A8%98%E9%8C%B2%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)で紹介した、__`board_records`__ 属性に代入された __list の要素__ に、__それぞれの手数の局面を表すデータを代入する__ という方法を採用してきました。この方法は __プログラムの記述が簡単に行える__ という利点がある一方で、__`board_records`__ 属性に __最大で__ 0 手目から 9 手目までの、__10 個の局面のデータを保存する必要がある__ という欠点があります。

__`Marubatsu`__ クラスのインスタンスの __属性に記録するデータの量が増える__ と、その分だけ AI が着手を行う際に実行する __`deepcopy` の処理に時間がかかってしまう__ ため、ゲーム盤のデータを記録する __前と後__ では、__AI が対戦したときにかかる時間が増えてしまいます__。

そのことを示すために、下記のプログラムで __`ai1s` どうし__（他の AI でもかまいません）が __10000 回対戦した時にかかる時間を計測__ してみることにします。実行結果は重要ではないので省略しますが、筆者のパソコンで下記のプログラムを実行すると __約 67 秒__ かかりました。

```python
from ai import ai_match

ai_match(ai=[ai1s, ai1s])
```

## 着手の記録を使った局面の再現の仕組み

__`Marubatsu`__ クラスのインスタンスには、各手数で行われた __着手を記録する `records` 属性__ が存在しますが、__このデータを利用__ すれば、__`board_records` 属性のデータが存在しなくても__ 、__好きな手数の局面を再現する__ ことができます。その方法について少し考えてみて下さい。

例えば、3 手目の局面は以下の手順で再現できます。

* ゲーム盤をゲーム開始時の状態にする
* `records` 属性から 3 手分の着手のデータを順番に取り出し、着手を行う

この方針に従って、任意の手数の局面に移動する `Marubatsu` クラスの __`change_step`__ メソッドを修正する方法について少し考えてみて下さい。

:::note
各手数で行われた着手のデータは、__前の局面と次の局面の差異を表すデータ__ なので、このようなデータのことを __差分データ__ と呼びます。

〇×ゲームに限らず、オセロ、将棋、囲碁などのデータは、__差分を表す着手のデータがあれば、任意の局面のデータを再現できる__ ので、すべての局面のデータを記録するかわりに、__着手のデータだけを記録__ することで、__記録する必要があるデータの量を大幅に削減する__ ことが良く行われます。

例えば、将棋のゲーム盤は 81 マスあるので、将棋の各局面のデータを記録するためにはかなりの量のデータが必要なりますが、着手のデータは局面のデータと比較すると数十分の一以下の小さなデータで表現できます。また、将棋はゲームが終了するまで平均すると約 100 回の着手が行われるので、着手のデータだけを記録することで、記録するデータを実際に大幅に削減することができます。

余談ですが、動画は画像を高速にパラパラ漫画のように切り替えて表示するという仕組みになっています。それぞれの画像の内容はほとんど変わらないので、すべての画像をそのまま記録する代わりに、前の画像と次の画像の差分のデータだけを記録することで、動画のデータを大幅に小さくするという工夫が行われています。
:::


## `board_records` 属性の削除

まず、__不要になった `board_records` 属性を削除する__ ことから始めることにします。`board_records` 属性は多くの場所で記述されているので少々面倒ですが、VSCode の __Ctrl + F による検索機能__ を使って、__`board_records` を検索すると良い__ でしょう。

下記は、`restart` メソッドから `board_records` を削除したプログラムです。

```python
def restart(self):
元と同じなので省略
    self.records = [self.last_turn]
    # この下にあった board_records 属性を初期化する処理を削除する
    
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.last_turn]
    
Marubatsu.restart = restart
```
</details>

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

```diff_python
def restart(self):
元と同じなので省略
    self.records = [self.last_turn]
-   self.board_records = [deepcopy(self.board)]      

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

下記は、`move` メソッドから `board_records` を削除したプログラムです。なお、`board_records` 属性を削除することで、元のプログラムの __`len(self.board_records)` を修正__ する必要があります。__`board_records` の要素の数は、`records` 属性の要素の数と同じ__ なので、`len(self.records)` に修正します。

* __3 行目__：`len(self.board_records)` を `len(self.records)` に修正する
* 4、7、10 行目の下にあった `board_records` 属性に関する処理を削除する

```python
 1  def move(self, x, y):
 2      if self.place_mark(x, y, self.turn):
元と同じなので省略
 3          if len(self.records) <= self.move_count:            
 4              # この下にあった board_records 属性に関する処理を削除する
 5              self.records.append(self.last_move)
 6          else:
 7              # この下にあった board_records 属性に関する処理を削除する
 8              self.records[self.move_count] = self.last_move
 9              self.records = self.records[0:self.move_count + 1]
10              # この下にあった board_records 属性に関する処理を削除する
11
12  Marubatsu.move = move
```

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

```python
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
        if len(self.records) <= self.move_count:            
            self.records.append(self.last_move)
        else:
            self.records[self.move_count] = self.last_move
            self.records = self.records[0:self.move_count + 1]

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

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

```diff_python
def move(self, x, y):
    if self.place_mark(x, y, self.turn):
元と同じなので省略
-       if len(self.board_records) <= self.move_count:            
+       if len(self.records) <= self.move_count:            
-           self.board_records.append(deepcopy(self.board))
            self.records.append(self.last_move)
        else:
-           self.board_records[self.move_count] = deepcopy(self.board)
            self.records[self.move_count] = self.last_move
            self.records = self.records[0:self.move_count + 1]
-           self.board_records = self.board_records[0:self.move_count + 1]

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

下記は、上記と同様に `update_widgets_status` メソッドの __`len(self.board_records)` を修正__ したプログラムです。

* __5、6 行目__：`len(self.board_records)` を `len(self.records)` に修正する

```python
1  def update_widgets_status(self):   
2      # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
3      self.set_button_status(self.first_button, self.mb.move_count <= 0)
4      self.set_button_status(self.prev_button, self.mb.move_count <= 0)
5      self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
6      self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)          
7
8  Marubatsu_GUI.update_widgets_status = update_widgets_status
```

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

```python
def update_widgets_status(self):   
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    self.set_button_status(self.first_button, self.mb.move_count <= 0)
    self.set_button_status(self.prev_button, self.mb.move_count <= 0)
    self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
    self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)          

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

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

```diff_python
def update_widgets_status(self):   
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    self.set_button_status(self.first_button, self.mb.move_count <= 0)
    self.set_button_status(self.prev_button, self.mb.move_count <= 0)
-   self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
+   self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
-   self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)          
+   self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)          

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

下記は、上記と同様に `create_event_handler` メソッドの __`len(self.board_records)` を修正__ したプログラムです。

* __3 行目__：`len(self.board_records)` を `len(self.records)` に修正する

```python
1  def create_event_handler(self):
元と同じなので省略
2      def on_last_button_clicked(b):
3          change_step(len(self.mb.records) - 1)
元と同じなので省略
4
5  Marubatsu_GUI.create_event_handler = create_event_handler
```

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

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

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b):
        self.mb.restart()
        on_change_button_clicked(b)
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b):
        change_step(0)

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

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

    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)
            
    # fig の画像にマウスを押した際のイベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    
               
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

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

```diff_python
def create_event_handler(self):
元と同じなので省略
    def on_last_button_clicked(b):
-       change_step(len(self.mb.board_records) - 1)
+       change_step(len(self.mb.records) - 1)
元と同じなので省略

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

`change_step` メソッドについては、この後でまとめて修正することにします。

## `change_step` メソッドの修正方法

`change_step` の修正を、下記のプログラムのように行えば良いと思ったはいないでしょうか？この発想は悪くないと思いますが、実際にはこの修正ではうまくいきません。

* __3 行目__：`len(self.board_records)` を `len(self.records)` に修正する
* __4 行目__：ゲームをリセットして、ゲームの開始時の局面にする
* __5、6 行目__：__`records`__ 属性から __1 ~ step 手目の着手__ を繰り返し処理を使って __順番に取り出し、`move` メソッドで着手を行う__。__`records`__ 属性の __0 番の要素__ には __`None` が代入__ されているので、__1 番の要素から取り出す必要がある__ 点に注意する事

なお、元のプログラムで行っていた、__`board` や `move_count` などを更新する処理__ は __`move` メソッド内で行われる__ ので __記述する必要はありません__。

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

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

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

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

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

```diff_python
def change_step(self, step):
    # step の範囲を正しい範囲に修正する
-   step = max(0, min(len(self.board_records) - 1, step))
+   step = max(0, min(len(self.records) - 1, step))
+   self.restart()
+   for x, y in self.records[1:step+1]:
+       self.move(x, y)
    # 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.last_move = self.records[step]

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

上記の修正後に、下記のプログラムで __`gui_play`__ を実行し、__いくつかの着手を行った後でリプレイ機能のボタンをクリック__ すると、__どのような場合__ でも実行結果のように、__ゲーム開始時の局面が表示__ され、__すべてのリプレイ機能のボタンが灰色で表示__ されて操作できなくなります。このような現象が起きる原因について少し考えてみて下さい。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/417791f8-b6aa-a028-2448-a2e81bbae43a.png" width="250">

## 問題の原因の検証

この問題の原因は、__`change_step`__ の中の __`self.restart()`__ にあります。__`restart`__ メソッドは、下記のプログラムの 3 行目のように、ゲーム盤をゲーム開始時の状態にするだけではなく、__`records` 属性の値も `[self.last_turn]` で初期化してしまう__ からです。

```python
1  def restart(self):
略
2      self.last_turn = None
3      self.records = [self.last_turn]
```

従って、下記の __`change_step`__ は、仮引数 `step` にどのような値が代入されていた場合でも、下記のような処理が行われ、画面にはゲーム開始時の局面が表示されるようになります。

* __`self.last_turn`__ には、上記の `restart` の 2 行目で __`None` が代入される__ ので、__`self.restart()` を実行後の `self.records`__ には必ず __`[None]` が代入される__
* __`for x, y in self.records[1:step+1]:`__ で __1 ~ step 番の要素__ を取り出そうとしても、 `self.records` には 0 番の要素しか代入されておらず、__そのような要素は存在しない__ ため、__繰り返し処理のブロック__ は __一度も実行されない__
* 結果として、`change_step` では、`self.restart()` によってゲームが初期化された後に __何の処理も行われない__ ので、__ゲーム開始時の局面の状態になる__

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

## 問題の修正

この問題は、__`restart`__ メソッドを実行すると、__`records` 属性の値が別の値で置き換わってしまう__ ことが原因です。従って、下記のプログラムのように、__`restart` メソッドを実行する前__ に、__`records` 属性の値を別の変数に代入して取っておく__ という方法が考えられます。

* __6 行目__：`self.records` の値をローカル変数 `records` に代入して取っておく
* __8 行目__：取っておいた `records` を使って繰り返し処理を行うように修正する

```python
 1  def change_step(self, step):
 2      # step の範囲を正しい範囲に修正する
 3      step = max(0, min(len(self.records) - 1, step))
 4      # self.records の値は restart メソッドを呼び出すと初期化されるので
 5      # ローカル変数 records に代入して取っておく
 6      records = self.records
 7      self.restart()
 8      for x, y in records[1:step+1]:
 8          self.move(x, y)
 9
10  Marubatsu.change_step = change_step 
```

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

```python
def change_step(self, step):
    # step の範囲を正しい範囲に修正する
    step = max(0, min(len(self.records) - 1, step))
    # self.records の値は restart メソッドを呼び出すと初期化されるので
    # ローカル変数 records に代入して取っておく
    records = self.records
    self.restart()
    for x, y in records[1:step+1]:
        self.move(x, y)

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

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

```diff_python
def change_step(self, step):
    # step の範囲を正しい範囲に修正する
    step = max(0, min(len(self.records) - 1, step))
    # self.records の値は restart メソッドを呼び出すと初期化されるので
    # ローカル変数 records に代入して取っておく
+   records = self.records
    self.restart()
-   for x, y in self.records[1:step+1]:
+   for x, y in records[1:step+1]:
        self.move(x, y)

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

ところで、上記のプログラムの 4 行目で、`records = deepcopy(self.records)` のように、`self.records` をコピーする必要があるのではないかと思った人はいないでしょうか？

上記の場合は __コピーを行う必要はありません__。その理由は、__`restart` メソッドの中__ で、__`self.records`__ に __別の新しい list を代入__ するという処理を行っているからです。

__`self.records`__ に __別の値を代入__ すると、__`records`__ と __`self.records`__ の __共有が解除__ され、__別々の list のデータが代入__ されることになります。そのため、__`restart`__ メソッドを実行しても、__`records` の値が変化することはありません__。また、この後で __`move`__ メソッドを実行して __`self.records` の要素が変化しても、`records` の値が変化することはありません__。

:::note
`records = deepcopy(self.records)` でデータをコピーしてもプログラムは正しく動作しますが、`copy` や `deepcopy` による __データのコピー__ は、代入による __データの共有の処理より__ も、__大幅に処理に時間がかかる__ ので、必要がなければ `deepcopy` や `copy` は利用しないほうが良いでしょう。
:::

上記の修正後に `gui_play` を実行し、いくつかの着手を行った後でリプレイ機能の << や < ボタンをクリックすると、正しい局面が表示されるようになります。しかし、<strong>> と >> ボタンが灰色で表示されて操作できない</strong>という問題が発生します。

例えば、3 手目の局面で < をクリックすると、実行結果のような画面が表示されます。このような現象が起きる原因について少し考えてみて下さい。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b61eea7d-aaf5-f871-7980-a4ebb9609db1.png" width="250">

## 新たな問題の検証

&gt; と >> ボタンが灰色で表示されるということは、__その局面が最後に着手が行われた局面__ であることを意味します。そのことを念頭において、__`change_step` で行われる処理を検証__ することにします。下記の `change_step` では、下記のような処理が行われます。

* ローカル変数 `records` に `self.records` が代入される
* `self.restart()` でゲームがリセットされる
* 1 ~ step 手目の着手が `records` から順番に取り出され、`move` メソッドで着手が行われる

```python
def change_step(self, step):
    # step の範囲を正しい範囲に修正する
    step = max(0, min(len(self.records) - 1, step))
    # self.records の値は restart メソッドを呼び出すと初期化されるので
    # ローカル変数 records に代入して取っておく
    records = self.records
    self.restart()
    for x, y in records[1:step+1]:
        self.move(x, y)
```

__`move`__ メソッドの処理では、__`records`__ 属性に、行われた __着手のデータが追加__ されます。`change_step` ではゲーム __開始時の局面から `step` 回の着手の処理が行われる__ ので、__`records`__ の要素には __0 ~ `step` 手目の着手のデータ__ が代入されます。従って、__`step` 手目が最後に行われた着手__ になるため、> と >> ボタンが灰色で表示されることになります。

この問題は、下記のプログラムの 8 行目のように `self.records` に、ゲームをリセットする前に取っておいた __`records` を代入しなおして着手の記録を元に戻す__ ことで解決できます。

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

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

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

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

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

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

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

上記の修正後に下記のプログラムを実行して `ai1s` どうしの対戦を 10000 回行うことで、処理時間が短縮されたことを確認することにします。

```python
ai_match(ai=[ai1s, ai1s])
```

筆者のパソコンでは上記のプログラムは __約 45 秒__ かかりました。__`board_records` 属性を削除する前は 約 67 秒__ だったので、__`board_records` 属性を削除__ することで、__AI の処理の時間が約 2/3 になり、実際にかなり短縮された__ ことを確認することができました。

なお、処理時間が短縮された理由は、AI が着手を選択する際に行う `deepcopy` の処理の時間が短くなっただけではありません。`Marubatsu` クラスから `board_records` 属性を削除することで、それまで、`move` で行っていた `board_records` の要素にゲーム盤の局面のデータをコピーして追加するという処理が行われなくなったことも、処理時間の短縮の原因です。

:::note
`board_records` を削除することで、`change_step` の中で、特定の局面を再現するためには、ゲーム開始時の局面からその局面までの着手を行うという処理が必要になります。そのため、`change_step` が行う処理は、`board_records` を削除することで、処理に必要な時間が増加しています。そのことが AI の処理時間の増加につながるのではないかと心配する人がいるかもしれませんが、そのようなことはおきません。

その理由は、`change_step` が GUI のリプレイ機能のボタンを押した場合でのみ呼び出されるため、AI が着手を行う際に呼び出されることはないからです。
:::

## 他の修正方法

他の修正方法として、__`restart`__ メソッドと __`move`__ メソッドに、__`True` を代入することで `records` 属性を変更しない__ ようにする __仮引数を追加__ するという方法が考えられますが、上記で紹介した方法よりも実装が面倒なので、本記事では採用しません。また、marubatsu.ipynb にも記述しないので、興味がある方は自分で記述してみて下さい。

具体的には、`restart` メソッドを下記のように修正します。

* __1 行目__：仮引数に、デフォルト値を `True` とする `update_records` を追加する。デフォルト値を `True` としたのは、これまでのプログラムとの互換性を保つためである
* __8、9 行目__：`update_records` が `True` の場合のみ、`records` 属性を変更する

```python
 1  def restart(self, update_records=True):
 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      if update_records:
 9          self.records = [self.last_turn]
10
11  Marubatsu.restart = change_restart
```

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

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

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

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

```diff_python
-def restart(self):
+def restart(self, update_records=True):
    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.last_turn]
+   if update_records:
+       self.records = [self.last_turn]

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

プログラムは省略しますが、`move` メソッドも同様の方法で修正します。

次に、`change_step` を下記のプログラムの 4、6 行目のように修正し、__`restart` メソッドと `move` メソッドを実行しても `records` 属性の値が変化しない__ ようにします。

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

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

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

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

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

```diff_python
def change_step(self, step):
    # step の範囲を正しい範囲に修正する
    step = max(0, min(len(self.records) - 1, step))
-   self.restart()
+   self.restart(update_records=True)
    for x, y in self.records[1:step+1]:
-       self.move(x, y)
+       self.move(x, y, update_records=True)

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

他にも方法はあるかもしれませんので、本記事で紹介した方法よりも良い方法を思いついた方は実装し、コメントで知らせてくれるとうれしいです。

# 今回の記事のまとめ

今回の記事では、GUI の機能の分離の作業を完了しました。また、差分によって局面を再現することで、インスタンスの属性に代入されたデータ量を減らし、`deepcopy` によるコピーに必要な時間を減らす方法を紹介しました。

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

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

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

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

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

# 次回の記事

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

[^1]:`set_button_status` は静的メソッドとして定義されていますが、__静的メソッドであるかどうかはこのエラーとは関係ありません__。このエラーは `set_button_status` を通常のメソッドとして定義した場合でも発生します