Pythonで〇×ゲームのAIを一から作成する　その81　AI が手番を担当した場合のリプレイ機能とGUIの処理の分離

# 目次と前回の記事

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

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

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

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

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

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

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

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

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

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

# AI が手番を担当した場合のリプレイ機能の実装

前回の記事では、リプレイ機能を実装する際に、人間どうしの対戦を行いましたが、__AI が手番を担当した場合__ では、この後で示すように __リプレイ機能がうまく働きません__。

例えば、下記のプログラムで `gui_play` を実行して __AI どうしの対戦を GUI で行う__ と、実行結果のように、__すべてのリプレイのボタンが灰色で表示__ され、__リプレイの操作ができない__ という問題が発生します。なお、対戦する AI はどの AI でも構いません。このような問題が起きる原因について少し考えてみて下さい。


```python
from util import gui_play
from ai import ai1

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/91eb6814-4854-f619-8da2-601569b80518.png" width="200">

## 問題の原因の検証

__リプレイボタンの設定__ は、__[`update_widgets_status`](https://qiita.com/ysgeso/items/94fba56cb97dc4787ca8#update_widgets_status-%E3%81%AE%E5%AE%9A%E7%BE%A9)__ を呼び出すことで行いますが、この処理は現時点では下記の場合でのみ行われています。

* `play` メソッド内で、`restart` メソッドを呼び出してゲームをリセットした直後
* リセットボタンをクリックした時のイベントハンドラの中
* ゲーム盤の上でマウスを押した時のイベントハンドラの中で、着手を行った場合
* リプレイボタンをクリックした際に呼び出される `change_step` の中

:::note
__`update_widgets_status`__ がどこで呼び出されているかを確認する場合は、VSCode の __Ctrl + F による検索機能__ を利用すると良いでしょう。
:::

また、AI どうしが対戦を行う場合は以下のような処理が行われます。

1. `play` メソッドの実行または、リセットボタンをクリックした際にゲームが開始される
2. いずれの場合も __`update_widgets_status`__ が呼び出され、ゲームの開始時は 0 手目の局面なので、__すべてのリプレイに関するボタンが灰色で表示__ され、__操作ができなくなる__
3. その後、`play_loop` メソッドによって AI どうしの対戦が決着がつくまで行われる
4. ゲームの決着後に `draw_board` メソッドが呼び出され、決着後のゲーム盤が描画される
5. ゲームの開始から決着がつくまでの間に __`update_widgets_status`__ が __呼び出されることはない__ ので、__すべてのリプレイに関するボタン__ は __灰色で表示されたまま__ になる

これが AI どうしで対戦を行った際にリプレイ機能が利用できない原因です。

## `update_widgets_status` を呼び出す必要がある場所の検証

これまでは、リプレイ機能に関するボタンの設定の更新を行う必要がある状況が __新しく判明した時点__ で、__その都度__ 必要な場所に __`update_widgets_status` を記述してきました__ 。

このような、プログラムで __特定の処理を行う必要がある場合__ に、その処理を行う必要がある場面が __見つかってからその処理を記述する__ という __場当たり的な方法__ は、実際に良く行われる方法ですが、その方法では、__以下の 2 つが区別できない__ という問題があります。

* プログラムに特定の処理を記述する必要がある場所がもう __存在しない__
* プログラムに特定の処理を記述する必要がある場所が __存在するが、見つかっていない__

もちろん、特定の処理をどこに記述する必要があるかが良くわかっていない場合は、見つかってから対処するしかないのですが、特定の処理を __どこに記述する必要があるかがはっきりとわかる場合__ は、その場所を明確にすることで、__必要な場所に確実にその処理を記述できる__ ようになります。

:::note
特定の処理を記述する必要な場所が見つかってから対処するという方法は、以前の記事で説明した、__経験から学ぶ__ という __[帰納法](https://qiita.com/ysgeso/items/d7f60ba34f167f77a87e#%E5%B8%B0%E7%B4%8D%E6%B3%95%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B)に相当__ します。以前の記事で説明したように、__帰納法__ では特定の処理をどこに記述する必要があるかについて、__100 % 正しい答えを見つけることはできません__ が、__理屈がわからなくても__、経験からその場所を見つけることができます。一方、__理屈から特定の処理をどこに記述するかを考える[演繹法](https://qiita.com/ysgeso/items/d7f60ba34f167f77a87e#%E6%BC%94%E7%B9%B9%E6%B3%95)__ を使えば、特定の処理を記述する必要がある場所を __100 % 見つけることができます__ が、理屈が良くわからない場合や、理屈から具体的な方法を見つけることが困難が場合は利用することはできません。
:::

そこで、リプレイ機能に関する __ボタンの設定の更新を必ず行う必要がある状況を検証__ することにします。リプレイ機能に関するボタンの設定の更新を必ず行う必要がある状況が何であるかについて少し考えてみて下さい。


:::note
特定の処理を行う必要がある場所がはっきりしないという状況がピンとこない人は、特定の処理を「__バグを修正する__」だと考えてみて下さい。

プログラムにはバグはつきものですが、プログラムが長くなればなるほど、プログラムの中に潜むバグを見つけることは困難になります。例えば 1 万行のプログラムの中に潜むバグを、プログラムを 1 行ずつ確認して探し出すことは大変です。

そのため、プログラムの __バグの多く__ は、__バグが発生してから原因を検証して対処する__ という方法を __取らざるを得ません__。実際に本記事でもこれまで何度もバグが発生してから対処するという方法を取ってきました。

一方、__特定の処理を行った場合に発生するバグ__ に関しては、プログラムがどれだけ長くても、__その処理に関連する部分だけに注目__ してバグを修正することができます。ただし、関連する部分が多い場合、やはりバグを見つけるのは困難です。
:::

下記の `update_widgets_status` が 4 つのリプレイに関するボタンに対して行う処理は、`self.move_count <= 0` と `self.move_count >= len(self.board_records) - 1` の式からわかるように、いずれも __現在の手数__ を表す __`self.move_count` の値に応じてボタンの設定を変更__ する 処理を行います。従って、リプレイ機能に関するボタンの設定は、__ゲームの手数が変更された際に行う__ 必要があります。

```python
def update_widgets_status():
    set_button_status(first_button, self.move_count <= 0)
    set_button_status(prev_button, self.move_count <= 0)
    set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
    set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
```

上記から、ゲームの手数を変更する処理を探し出し、その直後に `update_widgets_status` を呼び出す処理を記述すれば良いことがわかりますが、もっと簡単な方法があるのでその方法を少し考えてみて下さい。

__ゲームの手数が変更された場合__ は、必ず __ゲーム盤の表示を更新する必要がある__ ので、その処理を行う __`draw_board`__ メソッドが __呼び出す必要があります__。つまり、__`draw_board` メソッドの中に `update_widgets_status` を呼び出す処理を記述する__ ことで、ゲームの手数が変更された場合に必ず `update_widgets_status` が呼び出されるようになります。

:::note
細かい話になりますが、手数を変更する処理の後に毎回 `update_widgets_status` を記述するよりも、`draw_board` の中に `update_widgets_status` を記述したほうが良い理由が他にもあります。それは、AI どうしの対戦を行う場合です。

__AI どうしの対戦__ が行われた場合は、すぐに決着がつくため、対戦の __途中経過のゲーム盤の画面を描画__ しても __対戦結果のゲーム盤の描画で上書き__ されてしまいます。そのため、`play_loop` の下記のプログラムの 9、10 によって、__必要のない途中経過の画面を描画しない__ という処理を行っています。

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

これは、リプレイに関するボタンの設定も同様で、AI どうしの対戦の __途中の局面__ に対して、__リプレイに関するボタンの設定を行う処理__ を行っても、すぐに決着がついた局面でのリプレイボタンの設定で上書きされてしまうため __意味はありません__。`draw_board` の中で `update_widgets_status` を呼び出すようにすることで、対戦の途中で __無駄な `update_widgets_status` の呼び出しが行われなくなります__。
:::

## `draw_board` メソッドからの `update_widgets_status` の呼び出し

上記の処理は、下記のプログラムの 10 行目のように、`draw_board` のブロックの中の最後に `update_widgets_status` の呼び出しを記述すれば良いと思う人がいるかもしれません。

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

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

```python
from marubatsu import Marubatsu

def draw_board(self, ax, ai):           
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.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.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn
    # 引き分けの場合
    elif self.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.BOARD_SIZE):
        ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線   

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

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

```diff_python
from marubatsu import Marubatsu

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


しかし、上記の修正後に、下記のプログラムで `gui_play` を実行すると、実行結果のようなエラーが発生します。このエラーの原因について少し考えてみて下さい。

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

実行結果

```
略
Cell In[2], line 46
     43         color = "red" if (x, y) == self.last_move else "black"
     44         self.draw_mark(ax, x, y, self.board[x][y], color) 
---> 46 update_widgets_status()

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

エラーメッセージから、__`update_widgets_status` が定義されていない__ ことがわかります。これは、__`update_widgets_status`__ が __`play` メソッドのローカル関数として定義されている__ ため、__`play` メソッドの外__ の `draw_board` メソッドでは __利用できない__ からです。

:::note
[以前の記事](https://qiita.com/ysgeso/items/c91d5373753253d98f23#draw_board-%E3%81%B8%E3%81%AE%E4%BB%AE%E5%BC%95%E6%95%B0-ax-%E3%81%AE%E8%BF%BD%E5%8A%A0)でも説明しましたが、定義されていない名前は VSCode では下図のようにオレンジ色の波線が表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/50a10991-e629-5ddc-9f91-e7f5f6eafb71.png" width="350">
:::

そこで、下記のプログラムのように、__`update_widgets_status` の定義__ を、__`play` メソッドの中にコピーすればよい__ と思う人がいるかもしれません。なお、`update_widgets_status` の中で、`set_button_status` を呼び出しているので、`set_button_status` もコピーしました。

```python
 1  def draw_board(self, ax, ai):           
元と同じなので省略
 2      # ボタンのウィジェットの状態を設定する
 3      def set_button_status(button, disabled):
 4          button.disabled = disabled
 5          button.style.button_color = "lightgray" if disabled else "lightgreen"
 6
 7      # ウィジェットの状態を更新する        
 8      def update_widgets_status():
 9          # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
10          set_button_status(first_button, self.move_count <= 0)
11          set_button_status(prev_button, self.move_count <= 0)
12          set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
13          set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
14
15      update_widgets_status()
16  
17  Marubatsu.draw_board = draw_board
```

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

```python
def draw_board(self, ax, ai):           
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.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.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn
    # 引き分けの場合
    elif self.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.BOARD_SIZE):
        ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線   

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

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

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

    update_widgets_status()
    
Marubatsu.draw_board = draw_board
```
</details>

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

```diff_python
def draw_board(self, ax, ai):           
元と同じなので省略
    # ボタンのウィジェットの状態を設定する
+   def set_button_status(button, disabled):
+       button.disabled = disabled
+       button.style.button_color = "lightgray" if disabled else "lightgreen"

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

    update_widgets_status()
    
Marubatsu.draw_board = draw_board
```
</details>

上記の修正後に、下記のプログラムで `gui_play` を実行すると、今度は実行結果のような別のエラーが発生します。このエラーの原因について少し考えてみて下さい。

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

実行結果

```
略
Cell In[4], line 52
     50 def update_widgets_status():
     51     # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
---> 52     set_button_status(first_button, self.move_count <= 0)
     53     set_button_status(prev_button, self.move_count <= 0)
     54     set_button_status(next_button, self.move_count >= len(self.board_records) - 1)

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

エラーメッセージから、今度は __`first_button` が定義されていない__ ことがわかります。これは、先程と同様に、__`first_button`__ が __`play` メソッドのローカル変数として定義されている__ からです。この問題を解決する方法について少し考えてみて下さい。

:::note
こちらの場合も、VSCode では下図のようにオレンジ色の波線が表示されます。エラーメッセージには `first_button` の事しか記述されていませんが、下図から `prev_button` なども定義されていないことがわかります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/787b8a53-d38e-be14-3f8b-e1f9c40d0351.png" width="500">
:::

### 属性によるデータの共有

`first_button` に関する処理を `draw_board` メソッドの中に移動すればよいと思う人がいるかもしれませんが、__`first_button` は、`play` メソッドの中で利用されている__ ので、`first_button` に関する処理を `draw_board` メソッドに移動すると今度は __`play` メソッドで `first_button` を利用する処理が動作しなくなる__ という問題が発生します。

これまで、__`update_widgets_status` の中で `first_button` をそのまま利用できていた__ のは、__`update_widgets_status`__ が、`first_button` をローカル変数として持つ __`play` メソッドのローカル関数として定義されていたから__ です。従って、`draw_board` メソッドを、`play` メソッドのローカル関数として定義すれば良いと思った人がいるかもしれませんが、その方法はうまくいきません。その理由は、__`draw_board`__ が __`play_loop`__ という、__`play` メソッド以外のメソッドから呼び出されている__ ため、`draw_board` メソッドを、`play` メソッドのローカル関数として定義すると、`play_loop` から `draw_board` を利用できなくなるからです。

この問題を解決する方法の一つは、__`first_button`__ をローカル変数ではなく、__`Marubatsu` クラスのインスタンスの属性とする__ というものです。そのようにすることで、`play` メソッドと `draw_board` メソッド内で、`self.first_button` と記述することで、`self.first_button` のボタンのウィジェットを共有することができるようになります。

下記は、`play` メソッドの修正です。なお、`set_button_status` と `update_widgets_status` は `draw_board` の中だけでしか利用しないので、それらの関数の定義を `play` メソッドの中から削除しました。また、`update_widget_status` の呼び出しは `draw_board` の中で行うようにしたため、必要がなくなったので `play` メソッドの中から削除しました。

* __12 行目の下__：__`set_button_status`__ と __`update_widgets_status`__ の __定義を削除__ する
* __16、28、43、45行目の下__：__`update_widgets_status`__ の __呼び出しを削除__ する
* __21 ~ 24、29 ~ 32、37 行目__：__`first_button`__ などの、ボタンのウィジェットを表すローカル変数の前に `self.` を記述して、__インスタンスの属性に修正__ する

```python
 1  import matplotlib.pyplot as plt
 2  import ipywidgets as widgets
 3  import math
 4  from copy import deepcopy
 5
 6  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 7      # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
 8      if gui:
 9          # %matplotlib widget のマジックコマンドを実行する
10          get_ipython().run_line_magic('matplotlib', 'widget')
11
12          # この下にあった set_button_status と update_widgets_status の定義を削除する
元と同じなので省略      
13          # リセットボタンのイベントハンドラを定義する
14          def on_reset_button_clicked(b):
15              self.restart()
16              # この下にあった update_widgets_status の呼び出しを削除する
17
18              on_change_button_clicked(b)
元と同じなので省略            
19          # 2 行目の UI を作成する
20          # リプレイのボタンを作成する
21          self.first_button = create_button("<<", 100)
22          self.prev_button = create_button("<", 100)
23          self.next_button = create_button(">", 100)
24          self.last_button = create_button(">>", 100)
元と同じなので省略        
25          def change_step(step):
元と同じなので省略        
26              # 描画を更新する
27              self.draw_board(ax, ai)
28              # この下にあった update_widgets_status の呼び出しを削除する
元と同じなので省略        
29          self.first_button.on_click(on_first_button_clicked)
30          self.prev_button.on_click(on_prev_button_clicked)
31          self.next_button.on_click(on_next_button_clicked)
32          self.last_button.on_click(on_last_button_clicked)
33
34          # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
35          hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
36          # リプレイ機能のボタンを横に配置した HBox を作成する
37          hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button]) 
38          # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
39          display(widgets.VBox([hbox1, hbox2]))        
元と同じなので省略        
40          # ローカル関数としてイベントハンドラを定義する
41          def on_mouse_down(event):
元と同じなので省略        
42                  self.draw_board(ax, ai)
43                  # この下にあった update_widgets_status の呼び出しを削除する
元と同じなので省略        
44      self.restart()
45      # この下にあった update_widgets_status の呼び出しを削除する
46
47      return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
48
49  Marubatsu.play = play
```

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

```python
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math
from copy import deepcopy

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"),
                style={"button_color": "lightgreen"},
            )
        
        # 変更、リセットボタンを作成する
        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 を作成する
        # リプレイのボタンを作成する
        self.first_button = create_button("<<", 100)
        self.prev_button = create_button("<", 100)
        self.next_button = create_button(">", 100)
        self.last_button = create_button(">>", 100)
        
        def change_step(step):
            # step が負の場合は 0 に修正する
            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)

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.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
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math
from copy import deepcopy

def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')

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

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

元と同じなので省略      
        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
-           update_widgets_status()
            on_change_button_clicked(b)
元と同じなので省略            
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
-       first_button = create_button("<<", 100)
+       self.first_button = create_button("<<", 100)
-       prev_button = create_button("<", 100)
+       self.prev_button = create_button("<", 100)
-       next_button = create_button(">", 100)
+       self.next_button = create_button(">", 100)
-       last_button = create_button(">>", 100)
+       self.last_button = create_button(">>", 100)
元と同じなので省略        
        def change_step(step):
元と同じなので省略        
            # 描画を更新する
            self.draw_board(ax, ai)
-           update_widgets_status()
元と同じなので省略        
-       first_button.on_click(on_first_button_clicked)
+       self.first_button.on_click(on_first_button_clicked)
-       prev_button.on_click(on_prev_button_clicked)
+       self.prev_button.on_click(on_prev_button_clicked)
-       next_button.on_click(on_next_button_clicked)
+       self.next_button.on_click(on_next_button_clicked)
-       last_button.on_click(on_last_button_clicked)
+       self.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]) 
+       hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
元と同じなので省略        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
元と同じなので省略        
                self.draw_board(ax, ai)
-               update_widgets_status()
元と同じなので省略        
    self.restart()
-   update_widgets_status()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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


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

* __5 ~ 8 行目__：`first_button` などのボタンのウィジェットが代入されているローカル変数の前に、`self.` を記述してインスタンスの属性に修正する

```python
 1  def draw_board(self, ax, ai): 
元と同じなので省略
 2      # ウィジェットの状態を更新する        
 3      def update_widgets_status():
 4          # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
 5          set_button_status(self.first_button, self.move_count <= 0)
 6          set_button_status(self.prev_button, self.move_count <= 0)
 7          set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
 8          set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
 9
10      update_widgets_status()    
11
12  Marubatsu.draw_board = draw_board
```

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

```python
def draw_board(self, ax, ai):           
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.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.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn
    # 引き分けの場合
    elif self.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.BOARD_SIZE):
        ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線   

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

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

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

    update_widgets_status()    

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

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

```diff_python
def draw_board(self, ax, ai): 
元と同じなので省略
    # ウィジェットの状態を更新する        
    def update_widgets_status():
        # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
-       set_button_status(first_button, self.move_count <= 0)
+       set_button_status(self.first_button, self.move_count <= 0)
-       set_button_status(prev_button, self.move_count <= 0)
+       set_button_status(self.prev_button, self.move_count <= 0)
-       set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
+       set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
-       set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
+       set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)

    update_widgets_status()

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

上記の修正後に、下記のプログラムで `gui_play` を実行して AI どうしの対戦を GUI で行うと、実行結果のように << と < ボタンが緑色で表示され、リプレイ機能を利用できるようになります。実際にリプレイ機能のボタンをクリックして確認して下さい。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/73c9fb62-94e8-6510-1a79-f8386de2e268.png" width="200">

また、図は省略しますが、人間 VS AI と AI VS 人間でもリプレイ機能を利用できるようになったことを確認して下さい。

# クラスによる GUI の機能の分離

ここまでで、リプレイ機能の主要な機能は完成しましたが、`play` メソッドの内容がかなり長く、複雑になりすぎてプログラムがわかりづらくなったと感じている人が多いかもしれません。また、以下のような点が気になっている人はいないでしょうか？

* `first_button` などのリプレイに関するボタンはインスタンスのメソッドに代入されているが、`reset_button` などのボタンはあいかわらずローカル変数に代入されている
* GUI に関する多くの関数は、`play` メソッドのローカル関数として定義されているが、`draw_board` は `Marubatsu` クラスのメソッドとして定義されている

上記のような、__統一性がないプログラム__ は、__バグの原因になる可能性が高くなる__ ので、__避けたほうが良い__ でしょう。

また、これまでのプログラムで、`first_button` や `update_widgets_status` などを、`Marubatsu` クラスの属性やメソッドではなく、`play` メソッドのローカル変数やローカル関数としてきたのは、以下のような理由からでした。

* それらが `play` メソッドの中でしか利用されないデータや関数であったため
* `Marubatsu` クラスの属性やメソッドを増やすと、クラスの構成が複雑になり、わかりづらくなるため

GUI に関する処理が少なければローカル変数やローカル関数で処理を記述してもあまり問題はありませんが、現状の `play` メソッドのように長く、複雑になった場合は、もっとわかりやすい方法で記述したほうが良いでしょう。

:::note
プログラミムの記述の方法に、唯一の正解はありません。また、人によってわかりやすいプログラムは異なります。データや関数をローカル変数やローカル関数で記述するか、属性やメソッドで記述するかは、一長一短があるので、どちらかが必ずしも優れているとは限らない点に注意して下さい。
:::

本記事で行ってきたように、__必要な機能を、必要になった時点で実装していく__ という方法でプログラムを記述すると、どうしてもプログラムの変数や関数などの構成が複雑になったり、統一性がなくなってわかりづらくなることが良くあります。そのような場合は、__プログラムの構成を整理して作り直すことが良く行われます__。そのようなことを体験することは、プログラミングの能力の向上につながると思いますので実例を紹介することにします。

:::note
現実の世界で例えると、例えば自分の部屋の本棚に購入した本を入れる場合、本の数が少ないうちは、その場でその本を入れる場所を考えて入れても大きな問題は発生しないでしょう。しかし、本の数がある程度以上増えてくると、本の配置に矛盾が生じたりしてわかりづらくなるため、本の配置を整理し直す必要が生じるでしょう。
:::

〇×ゲームの場合は、AI で対戦を行う機能を実装後に、`play` メソッド内に GUI に関する処理を追加して記述しましたが、__GUI に関する機能__ は、〇×ゲームを AI で遊ぶ際に必要となる __必須の機能ではありません__。また、GUI に関する処理を `Marubatsu` クラスの中に記述すると、必須の機能とそうでない機能が混ざってわかりづらくなります。そこで、`Marubatsu` クラスの中から __GUI に関する処理を分離する__ ことで、`Marubatsu` クラスの __構成をシンプルにしてわかりやすくする__ ことにします。具体的には、__GUI に関する処理__ を __別のクラスを定義して記述する__ ことで、__`Marubatsu` クラスから分離する__ ことにします。

なお、関数ではなく、クラスという形で分離することにしたのは、以下の理由からです。

* GUI に関する処理は、`play` メソッドで行うウィジェットの定義や配置などに関する処理と、`draw_board` メソッドで行うウィジェットの表示に関する処理がある。それらの処理を __クラスのメソッドとして定義する__ 事で、__まとめて扱うことができる__ ようになる
* ウィジェットの定義や配置などに関する処理と、ウィジェットの表示に関する処理では、AI やウィジェットの情報などの __共通する情報を扱う必要がある__。それぞれを関数で定義すると、共通する情報を仮引数に代入する必要が生じるが、クラスのメソッドとして定義した場合は、__それらの情報を属性に代入__ することで、__仮引数が必要なくなる__

後者の理由は、言葉だけの説明ではわかりづらいと思いますが、この後で行う実装を見れば、意味がわかるようになるのではないかと思います。

## `Marubatsu_GUI` クラスの記述の方針

作成するクラスの名前は、〇×ゲームの GUI の処理を行うので、__`Marubatsu_GUI`__ とします。

クラスには、`play` メソッド内に記述されていた __GUI に関する処理を記述__ しますが、__`play`__ メソッドと同じように記述するとわかりづらいので、下記の方針で記述することにします。

* __ウィジェットの作成__、__ウィジェットの配置__、__ウィジェットの表示の更新__、__イベントハンドラの定義と結び付け__ などの処理を行う __メソッドをそれぞれ定義__ する。そうする事で、__プログラムの処理が整理__ されて、わかりやすくなるという効果が得られる
* 必要がない限り、ローカル変数やローカル関数を使わずに、__属性とメソッドを利用__ する
* __`draw_board`__ メソッドは __GUI に関する処理__ なので __`Marubatsu_GUI` のメソッド__ にする

## `__init__` の定義

まず、インスタンスを作成した際に実行される __`__init__`__ メソッドの定義を行います。そのためには、__`__init__`__ の __仮引数を決める必要__ があります。もともと GUI の処理は `Marubatsu` クラスの `play` メソッド内で行われていた処理なので、`Marubatsu_GUI` クラスが行う __GUI の処理に必要となるデータ__ は、__`play` メソッドの仮引数の一部__ です。従って、__`__init__`__ メソッドの仮引数に必要となる、__`play` メソッドの仮引数を検証する__ ことにします。

`play` メソッドの仮引数と、`Marubatsu_GUI` での必要性は以下の通りです。なお、必要性が × となっている仮引数が、今後必要になった場合は、その都度追加することにします。

| 仮引数 | 意味 | 必要性 | 理由 |
|:--|:--|:-:|:--|
| __`self`__  | `Marubatsu` クラスのインスタンス  | 〇 | 様々な場面で必要  |
| __`ai`__  | 手番を担当する AI の list | 〇 | AI の処理を行う際に必要  |
| __`ai_dict`__   | Dropdown の AI の一覧  | 〇 | Dropdown の作成に必要  |
| __`params`__  | AI の関数に渡すパラメータ  | 〇 | AI の処理を行う際に必要 |
| __`verbose`__  | 途中経過を表示するか  | ×  | GUI で遊ぶ場合は必ず<br>`True` になるので不要 | 
| __`seed`__  | 乱数の種 | ×  | 乱数の種の処理は GUI<br>とは関係がないので不要 |
| __`gui`__  | GUI で遊ぶか  | ×  | GUI で遊ぶ場合は必ず<br> `True` になるので不要 |
| __`size`__  | ゲーム盤の画像のサイズ  | 〇  | ゲーム盤の描画の際に必要  |

なお、__`play`__ メソッドの __`self`__ は __`Marubatsu` クラスのインスタンス__ を表しますが、上記の __`__init__`__ のメソッドの __`self`__ は __`Marubatsu_GUI` クラスのインスタンス__ を表すので、__上記の表の `self`__ を代入する __`__init__`__ メソッドの __仮引数の名前__ は __`self` 以外の名前__ にする必要があります。そこで、本記事では __`mb` という名前にする__ ことにします。

下記は、__`Marubatsu_GUI`__ クラスと、__`__init__`__ メソッドの定義です。__`__init__`__ メソッドには、`play` メソッドの先頭に記述されていた処理の中で、GUI に関する処理の一部を記述しました。なお、乱数の種に関する処理は GUI には関係がないので記述していません。

* __2 行目__：上記の表の仮引数を記述する。デフォルト引数のデフォルト値は `play` メソッドと同じ値を設定した
* __4 ~ 8 行目__：`play` メソッド内の、仮引数 `ai_dict`、`params` に関する処理を記述する
* __10 ~ 14 行目__：__`__init__`__ メソッドの __仮引数__ は、一般的にそのクラスの __さまざまな場所で使われるデータ__ なので、下記のプログラムの 10 ~ 14 行目のように、__それぞれの仮引数の値__ を、__同じ名前の属性に代入するのが一般的__ である。そのようにすることで、__クラスのメソッド__ でそれらの値を __利用できるようになる__
* __17 行目__：`play` メソッド内で GUI の処理が記述されている、`if gui:` のブロックの最初で行われている、`%matplotlib widget` のマジックコマンドを実行する処理を記述する。なお、それ以降の処理は、この後で順次追加する

```python
 1  class Marubatsu_GUI:
 2      def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
 3          # ai_dict が None の場合は、空の list で置き換える
 4          if ai_dict is None:
 5              ai_dict = {}
 6          # params が None の場合のデフォルト値を設定する
 7          if params is None:
 8              params = [{}, {}]
 9
10          self.mb = mb
11          self.ai = ai
12          self.ai_dict = ai_dict
13          self.ai_params = params
14          self.size = size         
15        
16          # %matplotlib widget のマジックコマンドを実行する
17          get_ipython().run_line_magic('matplotlib', 'widget')
```

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

```python
class Marubatsu_GUI:
    def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
        # ai_dict が None の場合は、空の list で置き換える
        if ai_dict is None:
            ai_dict = {}
        # params が None の場合のデフォルト値を設定する
        if params is None:
            params = [{}, {}]

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

:::note
上記の 4、5、12 行目は、下記のプログラムのように 1 行で記述することもできます。7、8、13 行目も同様です。わかりやすいと思ったほうを採用して下さい。

```python
self.ai_dict = {} if ai_dict is None else ai_dict
```
:::

:::note
下記の `Marubatsu` クラスの `__init__` メソッドでも、上記と同様に、仮引数 `board_size` を同じ名前の属性に代入しています。なお、`BOARD_SIZE` 属性を大文字にしているのは、[以前の記事](https://qiita.com/ysgeso/items/ad5e7ddb071653fd2ac5#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E5%B1%9E%E6%80%A7%E3%81%B8%E3%81%AE%E5%80%A4%E3%81%AE%E4%BB%A3%E5%85%A5)で説明したように、後から `BOARD_SIZE` 属性に代入された値を変更する予定がない定数であることを明確にするためです。

```python
class Marubatsu:
    def __init__(self, board_size=3):
        # ゲーム盤の縦横のサイズ
        self.BOARD_SIZE = board_size
        # 〇×ゲーム盤を再起動するメソッドを呼び出す
        self.restart()
```
:::

## AI を選択する Dropdown を作成する関数の定義

`play` メソッドで次に記述されているのは、AI を選択する __Dropdown を作成する処理__ です。そこで、Dropdown を作成する __`create_dropdown`__ を下記のプログラムのように定義します。__基本的には `play` メソッド内の処理をコピー__ しますが、__以下の点が異なります__。なお、Dropdown を作成するために __必要なデータ__ は、`__init__` メソッド内で、__インスタンスの属性に代入済__ なので、__仮引数__ に __`self` 以外を記述する必要はありません__。これが、先程説明した、GUI の機能をクラスで定義する事の 2 つ目の理由です。

* __5、24 行目__：ウィジェットをローカル変数ではなく、インスタンスの属性に代入する
* __9、13、14、18、20、26 行目__：`ai` 等のローカル変数を `self.ai` に変更する

```python
 1  def create_dropdown(self):
 2      # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
 3      select_values = []
 4      # 〇 と × の Dropdown を格納する list
 5      self.dropdown_list = []
 6      # ai に代入されている内容を ai_dict に追加する
 7      for i in range(2):
 8          # ラベルと項目の値を計算する
 9          if self.ai[i] is None:
10              label = "人間"
11              value = "人間"
12          else:
13              label = self.ai[i].__name__        
14              value = self.ai[i]
15          # value を select_values に常に登録する
16          select_values.append(value)
17          # value が ai_values に登録済かどうかを判定する
18          if value not in self.ai_dict.values():
19              # 項目を登録する
20              self.ai_dict[label] = value
21    
22          # Dropdown の description を計算する
23          description = "〇" if i == 0 else "×"
24          self.dropdown_list.append(
25              widgets.Dropdown(
26                  options=self.ai_dict,
27                  description=description,
28                  layout=widgets.Layout(width="100px"),
29                  style={"description_width": "20px"},
30                  value=select_values[i],
31              )
32          )    
33    
34  Marubatsu_GUI.create_dropdown = create_dropdown
```

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

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

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

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

:::note
下記のプログラムの 2、3 行目のように、`self.ai` などをローカル変数に代入することで、`ai` と `ai_dict` を `self.ai` のように修正する必要が無くなります。

```python
def create_dropdown(self):
    ai = self.ai
    ai_dict = self.ai_dict
以下略
```

ただし、例えば `self.ai = None` などのように、`self.ai` や `self.ai_dict` に __直接値を代入する処理__ を __この後のプログラムで記述する必要がある場合__ は、__`ai`__ と __`self.ai`__ の __値が別のデータになってしまう__ ため __バグが発生する可能性が生じる__ 点に注意して下さい。

なお、`self.ai[0] = None` のように、要素の値を変更する場合は、`ai[0]` の値も同時に変更されるので問題はありません。

__上記の区別がついていない方は、このような修正は避けたほうが良いでしょう__。
:::

## ウィジェットを配置して表示する関数の定義

上記の関数はウィジェットを作成するだけなので、__ウィジェットを配置して表示__ する、__`display_widgets`__ という関数を下記のプログラムのように定義します。なお、現時点では、Dropdown しか作成していないので Dropdown を配置して表示する処理だけを記述します。

```python
def display_widgets(self):
    hbox = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1]])
    display(hbox) 
    
Marubatsu_GUI.display_widgets = display_widgets   
```

次に、下記のプログラムのように、`__init__` メソッドの最後の 5、6 行目に Dropdown を作成し、ウィジェットを表示するプログラムを記述します。

```python
1  def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
元と同じなので省略
2      # %matplotlib widget のマジックコマンドを実行する
3      get_ipython().run_line_magic('matplotlib', 'widget')
4    
5      self.create_dropdown()
6      self.display_widgets()
7    
8  Marubatsu_GUI.__init__ = __init__
```

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

```python
def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]

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

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

```diff_python
def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
元と同じなので省略
    # %matplotlib widget のマジックコマンドを実行する
    get_ipython().run_line_magic('matplotlib', 'widget')
    
+   self.create_dropdown()
+   self.display_widgets()
    
Marubatsu_GUI.__init__ = __init__
```
</details>

上記の修正後に、下記のプログラムを実行して `Marubatsu_GUI` クラスのインスタンスを作成すると、`Marubatsu_GUI` クラスの __`__init__`__ メソッドが実行されるので、実行結果のように Dropdown が表示されます。なお、`Marubatsu_GUI` の実引数には、__〇×ゲームのインスタンス__ と __人間 VS 人間__ を表すデータを記述しました。また、今夏の記事では利用しませんが、作成した `Marubatsu_GUI` クラスのインスタンスを `mb_gui` という名前の変数に代入しました。

```python
mb = Marubatsu()
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/2350ec90-1934-86c5-65b5-f04fce1bd146.png" width="200">

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

次に、__`play`__ メソッド内の、__ボタンを作成するローカル関数 `create_button`__ を __`Marubatsu_GUI` のメソッドとして定義__ します。なお、__`create_button`__ は、__`Marubatsu_GUI`__ の __インスタンスの情報を利用しない__ ので、以前の記事で説明した __`@staticmethod`__ のデコレータを使って __[静的メソッド](https://qiita.com/ysgeso/items/8ac83f1b50b2706b8319#%E9%9D%99%E7%9A%84%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%81%97%E3%81%A6%E3%81%AE-draw_mark-%E3%81%AE%E5%AE%9A%E7%BE%A9)__ として定義する事にしました。静的メソッドの意味が良くわからない人は、__通常のメソッドとして定義__ しても __問題はありません__。その場合は、__`@staticmethod` を削除__ し、__最初の仮引数に `self` を追加__ して下さい。

```python
@staticmethod
# ボタンを作成するローカル関数を定義する 
def create_button(description, width):
    return widgets.Button(
        description=description,
        layout=widgets.Layout(width=f"{width}px"),
        style={"button_color": "lightgreen"},
    )
    
Marubatsu_GUI.create_button = create_button
```

:::note
静的メソッドとして定義する事で、下記のプログラムのように __`Marubatsu_GUI` のインスタンスを作成しなくても__、`Marubatsu_GUI` の後に直接 `.create_button` を記述して __利用できる__ という利点が得られます。

```python
display(Marubatsu_GUI.create_button("リセット", 100))
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1d366af3-5743-a490-1ee0-177f9c078aa7.png" width="100">
:::

## ウィジェットをまとめて作成する関数の定義

次に、`create_button` を使って 6 つのボタンを作成する処理を `__init__` メソッドに直接記述しても良いのですが、ウィジェットの数が多くなるとわかりづらくなるので、下記のプログラムのように、__ウィジェットをまとめて作成__ する __`create_widgets`__ というメソッドを定義する事にします。先程 `__init__` メソッドに記述した Dropdown を作成する処理を記述し、その後で `play` メソッドに記述されていた 6 つのボタンを作成する処理を記述します。

特に難しい点はないと思いますので、修正箇所の説明などは省略します。

```python
def create_widgets(self):
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセットボタンを作成する
    self.change_button = self.create_button("変更", 100)
    self.reset_button = self.create_button("リセット", 100)
    # リプレイのボタンを作成する
    self.first_button = self.create_button("<<", 100)
    self.prev_button = self.create_button("<", 100)
    self.next_button = self.create_button(">", 100)
    self.last_button = self.create_button(">>", 100)    
    
Marubatsu_GUI.create_widgets = create_widgets
```

ウィジェットが増えたので、ウィジェットを配置して表示する `display_widgets` を下記のプログラムのように修正します。`play` メソッドの該当する処理の記述との違いは、それぞれのウィジェットが代入されたローカル変数の前に `self.` を記述するだけです。

```python
def display_widgets(self):
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button]) 
    # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2]))        
    
Marubatsu_GUI.display_widgets = display_widgets
```

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

```diff_python
def display_widgets(self):
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
-   hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
+   hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
-   hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
+   hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button]) 
    # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2]))        
    
Marubatsu_GUI.display_widgets = display_widgets
```
</details>

次に `__init__` メソッドを下記のプログラムのように修正します。

* __2 行目__：`create_dropdown` を `create_widgets` に修正する

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

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

```python
def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]

    self.mb = mb
    self.ai = ai
    self.ai_dict = ai_dict
    self.size = size
    
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        self.ai_dict = {}

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

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

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

上記の修正後に、下記のプログラムを実行して `Marubatsu_GUI` クラスのインスタンスを作成すると、実行結果のように Dropdown とボタンが表示されるようになります。なお、ボタンの設定の変更の処理や、イベントハンドラはまだ記述していないので、すべてのボタンの色は緑色で、クリックしても何も行われません。

```python
gui = Marubatsu_GUI(mb, ai=[None, None])
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/586bea1a-4475-42e3-1fa2-318cd1ba5594.png" width="350">

## ゲーム盤を表す Figure を作成する関数

次に、__ゲーム盤を表す Figure を作成__ する __`create_figure`__ を下記のプログラムのように定義します。なお、__`fig`__ や __`ax`__ は、__`draw_board`__ メソッド内で __ゲーム盤を描画する際などで必要となる__ ので、__`Marubatsu_GUI` クラスの属性に代入__ しました。

```python
def create_figure(self):
    self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False   
    
Marubatsu_GUI.create_figure = create_figure
```

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

```diff_python
def create_figure(self):
-   fig, ax = plt.subplots(figsize=[size, size])
+   self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
-   fig.canvas.toolbar_visible = False
+   self.fig.canvas.toolbar_visible = False
-   fig.canvas.toolbar_visible = False
+   self.fig.canvas.toolbar_visible = False
-   fig.canvas.footer_visible = False
+   self.fig.canvas.footer_visible = False
-   fig.canvas.resizable = False   
+   self.fig.canvas.resizable = False   
    
Marubatsu_GUI.create_figure = create_figure
```
</details>

次に、`create_widgets` メソッドを下記のプログラムのように修正します。

* __4 行目__：`create_figue` を呼び出して、ゲーム盤の画像を表す Figure を作成する

```python
1  def create_widgets(self):
元と同じなので省略
2      self.last_button = self.create_button(">>", 100)    
3      # ゲーム盤の画像を表す figure を作成する
4      self.create_figure()
5
6  Marubatsu_GUI.create_widgets = create_widgets
```

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

```python
def create_widgets(self):
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセットボタンを作成する
    self.change_button = self.create_button("変更", 100)
    self.reset_button = self.create_button("リセット", 100)
    # リプレイのボタンを作成する
    self.first_button = self.create_button("<<", 100)
    self.prev_button = self.create_button("<", 100)
    self.next_button = self.create_button(">", 100)
    self.last_button = self.create_button(">>", 100)    
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()
    
Marubatsu_GUI.create_widgets = create_widgets
```
</details>

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

```diff_python
def create_widgets(self):
元と同じなので省略
    self.last_button = self.create_button(">>", 100)    
    # ゲーム盤の画像を表す figure を作成する
+   self.create_figure()

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

作成した Figure は自動的に VSCode のセルに描画されるので、`display_widgets` を修正する必要はありません。上記の修正後に、下記のプログラムを実行して `Marubatsu_GUI` クラスのインスタンスを作成すると、実行結果のように Dropdown とボタンの下に Figure が表示されるようになります。ただし、ゲーム盤の画像を描画する処理はまだ記述していないので、ゲーム盤の画像はまだ描画されません。

```python
gui = Marubatsu_GUI(mb, ai=[None, None])
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0a2ac4fd-9af2-8343-aa3e-2db17be68520.png" width="350">

ゲーム盤の描画を行うメソッドの定義は少し長くなるので今回の記事はここまでにします。

# 今回の記事のまとめ

今回の記事では、__AI が手番を担当する場合__ の __リプレイ機能の問題を修正__ しました。

また、__GUI に関する処理__ を行う __`Marubatsu_GUI` クラスを定義__ し、__`Marubatsu`__ クラスの処理の中から __GUI の処理を分離__ する作業を開始しました。今回の記事では、`Marubatsu_GUI` のインスタンスを作成することで、GUI のウィジェットが表示される所まで実装しました。

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

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

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

以下のリンクは、今回の記事で更新した marubatsu.py です。なお、`Marubatsu_GUI` クラスは、〇×ゲームに関する処理であることには変わりがないので、`Marubatsu` クラスの後でその定義を記述することにします。

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

# 次回の記事

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