Pythonで〇×ゲームのAIを一から作成する　その79　リプレイ機能の < ボタンに関する修正

# 目次と前回の記事

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

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

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

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

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

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

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

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

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

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

# 強調について

__太字の強調__（←この表記のことです）の多用が見づらいという意見がコメントでありましたので、今回から半分以下に控えてみようと思います。もっと減らしたほうが良い、元の方が良かったなど、強調に限らず、意見があれば気軽にコメントしていただけると嬉しいです。

# リプレイ機能の問題点

前回の記事では、リプレイ機能を実装しましたが、以下のような問題点があります。今回の記事では、これらの問題点のいくつかを修正します。

* __直前の着手__ が __赤で表示されない__
* __ゲーム開始時__ の局面で __<__ ボタンをクリックすると __おかしな挙動__ が発生する
* __最後の着手__ が行われた局面で __>__ ボタンをクリックすると __エラーが発生__ する
* __最後の着手__ が行われた __以外__ の局面で着手を行うと __おかしな挙動__ が発生する

# 直前の着手が赤で表示されない

下記のプログラムで、`gui_play` を実行し、__いくつかの着手を行った後__ で、リプレイ機能のボタンをクリックして、__別の手数のゲーム盤を描画__ すると、実行結果のように、その手数のゲーム盤が描画されますが、その際に、__直前の着手__ のマークが __赤色で描画されない__ という問題が発生します。下図は、2 回着手を行った後に、__<__ ボタンをクリックした場合の図です。図のように、__1 手目の着手__ を表す 〇 が __黒色で描画__ されています。

```python 
from util import gui_play

gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/2050bc81-baa9-e460-a060-f500e8870bfa.png" width="250">

この問題は、リプレイ機能のボタンをクリックした際に実行される __`change_step`__ の中で、直前の着手を表す __`last_move`__ の内容が __変更されない__ ことが原因です。

各手数で行われた着手は、__`records`__ 属性に代入されているので、下記のプログラムの 13 行目のように、__`change_step`__ の中で、__`last_move`__ 属性に、__`records`__ 属性の中から __対応する直前の着手__ を表す要素を代入することで、この問題を解決することができます。なお、その処理を記述する際は、下記の点に注意する必要があります。

* ゲーム開始時の __0 手目の局面__ では、__直前に行われた着手__ は __存在しない__ ので、`None` を代入する必要がある
* __`records`__ 属性の __先頭の 0 番の要素__ には、__1 手目で行われた着手が記録__ されるので、__x 手目__ の局面の直前の着手は、__`records[x - 1]`__ に代入されている

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

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

```python
from marubatsu import Marubatsu
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"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

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

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

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)

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

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

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

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

```diff_python
インポートに関する記述は省略

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

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

実行結果は省略しますが、上記の修正後 に、下記のプログラムで `gui_play` を実行し、リプレイ機能のボタンをクリックしてこの問題が解決されたことを確認して下さい。

```python
gui_play()
```

# ゲーム開始時の局面で < ボタンをクリックするとおかしな挙動が発生する

下記のプログラムで、`gui_play` を実行し、以下の操作を行ってください。

* いくつかの着手を行う
* << ボタンをクリックして、ゲーム開始時の局面を表示する
* < ボタンをクリックする

```python 
gui_play()
```

上記の操作を行うと、以下のようなおかしなゲーム盤が表示されます。

* __最後に着手を行った局面__ が表示される
* 直前の着手を表す __赤いマークの位置がおかしい__

下図左は、(1, 1)、(0, 0)、(1, 0) の順で着手を行った場合の図です。下図右はその後で <<、< の順でボタンをクリックした場合の図で、__3 手目のゲーム盤が描画__ され、直前に行われた着手を表す __赤いマークの位置が左下図と異なります__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4b4a78d3-7080-fa71-e7a0-6da08c966d5f.png" width="250">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1e8b11a8-b61f-b2b6-971e-e80d9f9e79d5.png" width="250">

また、上記の後で、< ボタンをさらに 2 回クリックすると、下記のようなエラーが発生するという問題もあります。

```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[2], line 108
    107 def on_prev_button_clicked(b):
--> 108     change_step(self.move_count - 1)

Cell In[2], line 100
     98 self.status = self.judge()
     99 # 直前の着手を計算する
--> 100 self.last_move = None if step == 0 else self.records[step - 1]
    101 # 描画を更新する
    102 self.draw_board(ax, ai)

IndexError: list index out of range
```

なお、このエラーは、ゲームのリセット後に、一度も着手を行わずに < ボタンを押した場合でも発生します。

上記の 3 つの問題が起きる原因について少し考えてみて下さい。

## 問題の原因の検証

これらの問題は、[以前の記事](https://qiita.com/ysgeso/items/6ca248133ff4d59a81d4#%E8%B2%A0%E3%81%AE%E6%95%B4%E6%95%B0%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9)で説明した __負の整数のインデックス__ で行われる処理が原因です。上記では、ゲーム開始時の、__0 手目の局面__ で、__1 手前の局面を表示__ する < ボタンをクリックしました。__0 手目の局面__ では、手数を表す __`self.move_count`__ には __0 が代入されている__ ので、< ボタンをクリックした際に実行される下記のイベントハンドラでは、__-1 を実引数に記述__ して __`change_step`__ が呼び出されます。

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

### 最後に着手を行った局面が表示される原因

従って、下記のプログラムの `change_step` の __仮引数 `step`__ には __-1 が代入__ されるので、3 行目では、__`self.board_records[-1]`__ をコピーして `board` 属性に代入する処理が行われます。__list のインデックス__ に __負の整数 -n__ が記述された場合は、__後ろから n 番目の要素が参照される__ ことになるので、__`self.board_records[-1]`__ は、__最後の要素を表す__ ことになります。そのため、最後に着手を行った局面がコピーされて表示されることになります。

```python
1  def change_step(step):
2      # step 手目のゲーム盤のデータをコピーし、board に代入する
3      self.board = deepcopy(self.board_records[step])
4      # 手数を表す step を move_count に代入する
5      self.move_count = step     
略
6      # 直前の着手を計算する
7      self.last_move = None if step == 0 else self.records[step - 1]
8      # 描画を更新する
9      self.draw_board(ax, ai)
```

### 直前の着手を表す赤いマークの位置がおかしい原因

__`step`__ には __`-1` が代入されている__ ので、直前の着手を計算する 7 行目のプログラムでは、__`self.records[-2]`__ が `last_move` 属性に代入されます。これは、__最後から 2 番目に行われた着手__ なので、`last_move` には最後から 2 番目に行われた着手である (0, 0) が代入されます。これが、赤色で表示されるマークの位置がずれる理由です。

### < ボタンをクリックするとエラーが発生する原因

< ボタンをクリックするたびに、__`change_step(self.move_count - 1)`__ が呼び出され、`change_step` の 5 行目の __`self.move_count = step`__ が実行されるので、__`move_count`__ 属性の値が __1 ずつ減る__ ことになります。従って、ゲーム開始時の局面で < をクリックし、その後で < を 2 回クリックすると、__`move_count` 属性の値が -3__ になります。その状態で、上記の 7 行目を実行すると、__`self.records[-4]`__ が計算されますが、__`records`__ 属性に代入された __list には 3 手分の着手を表す要素しかない__ ので、後ろから 4 番目の要素を参照しようとすると、list の __インデックスが範囲外__ であることを表す list index out of range というエラーが発生します。これがエラーが発生する理由です。

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

## バグの修正方法その 1 

このバグの原因は、0 手目の局面で、< をクリックすると、__`move_count`__ 属性の値に -1 が代入されてしまうため、__-1 手目の局面__ という、__おかしな状態になってしまう__ ことです。そのため、このバグを修正する方法の一つは、__`move_count`__ 属性が __負の値にならないようにする__ ことです。具体的には、__負の値になってしまった時__ に、__0 に修正する__ という方法です。

下記はそのように __`change_step`__ を修正するプログラムで、最初に `step` の値を調べ、__負の値だった場合に 0 を代入する__ 処理を記述しています。

```python
def change_step(step):
    # step が負の場合は 0 に修正する
    if step < 0:
        step = 0
以下元と同じなので略
```

なお、この部分の処理は、下記のように 1 行で記述することができます。

```python
step = 0 if step < 0 else step
```

また、実引数の中の最大値を返す処理を行う、[以前の記事](https://qiita.com/ysgeso/items/93fb9b6daa59149514e9#%E8%A1%A8%E7%A4%BA%E3%81%AE%E6%94%B9%E8%89%AF%E3%81%9D%E3%81%AE-4count-%E3%81%A8-ratio-%E3%81%A7%E4%B8%8A%E4%B8%8B%E3%82%92%E6%8F%83%E3%81%88%E3%82%8B)で紹介した、組み込み関数 __`max`__ を使って下記のように、より簡潔に記述できます。

```python
step = max(0, step)
```

本記事は __`max`__ を採用し、下記のプログラムの 4 行目のように `play` メソッドを修正します。

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

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step が負の場合は 0 に修正する
            step = max(0, 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 = None if step == 0 else self.records[step - 1]
            # 描画を更新する
            self.draw_board(ax, ai)
        
        def on_first_button_clicked(b):
            change_step(0)

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

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

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

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)

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

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

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        def change_step(step):
            # step が負の場合は 0 に修正する
+           step = max(0, step)
            # step 手目のゲーム盤のデータをコピーし、board に代入する
            self.board = deepcopy(self.board_records[step])
元と同じなので省略            

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

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

```python
gui_play()
```

## バグの修正方法 その 2

上記の修正方法は、__`change_step`__ メソッドの中で、__`step`__ が __負の値にならないようにする__ というものでしたが、__`step` が負の値になるような場合__ に __< ボタンによる操作ができないようにする__ ことで、バグを修正することもできます。

### `disable` 属性によるボタンの操作の禁止

ipywidgets のウィジェットには、__`True` を代入__ することで __操作を禁止する__（行えないようにする）ことができる [__`disabled`__](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#disabled) 属性[^1]があり、__`move_count`__ 属性が 0 の場合に < ボタンのウィジェットの `display` 属性の値に `True` を、それ以外の場合に `False` を代入する処理を記述することで、この問題を修正することができます。__`self.move_count == 0`__ は、`self.move_count` に `0` が代入されている場合に `True` に、そうでない場合は `False` になるので、そのような処理は、下記のプログラムで記述することができます。

```python
# move_count が 0 の場合に < ボタンを操作できないようにする
prev_button.disabled = self.move_count == 0
```

:::note
__`==` 演算子の方が__、代入を行う __`=` よりも優先順位が高い__ ので、上記の式は先に `self.move_count == 0` が計算されてから代入処理が行われます。

上記の式がわかりづらいと思った方は、下記のように記述すると良いでしょう。

```python
prev_button.disabled = (self.move_count == 0)
```
:::

なお、__`self.move_count == 0`__ は、__`move_count`__ 属性が __負の値の場合は `False` になります__。先程 `self.move_count` の値が負の値にならないようにプログラムを修正しましたが、今後の修正や、思わぬバグのせいで __`move_count`__ 属性の値が __負の値になってしまうことがあるかもしれません__。その場合に < ボタンの操作ができてしまうのは不自然です。そのため、`move_count` が負の値にならないようにプログラムを記述していた場合でも、上記のプログラムは、下記のように記述したほうが良いでしょう。

```python
# move_count が 0 以下の場合に < ボタンを操作できないようにする
prev_button.disabled = self.move_count <= 0
```

### ボタンの操作を禁止する処理の記述場所

次に、上記のプログラムを __どこに記述するか__ を考える必要があります。これまでのプログラムでは、ゲーム開始時 に < ボタンを押せる状態になっていますが、__ゲーム開始時の手数は 0__ なので、少なくとも __ゲーム開始時にこの処理を行う必要があります__。下記は `play` メソッド内で、`restart` メソッドを実行してゲームを開始した直後の 4 行目に、上記の処理を記述するように修正したプログラムです。

```python
1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
2      self.restart()
3      # move_count が 0 以下の場合に < ボタンを操作できないようにする
4      prev_button.disabled = self.move_count <= 0
5      return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
6
7  Marubatsu.play = play
```

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

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

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step が負の場合は 0 に修正する
            step = max(0, 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 = None if step == 0 else self.records[step - 1]
            # 描画を更新する
            self.draw_board(ax, ai)
        
        def on_first_button_clicked(b):
            change_step(0)

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

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

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

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)

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

    self.restart()
    # move_count が 0 以下の場合に < ボタンを操作できないようにする
    prev_button.disabled = self.move_count <= 0
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
    self.restart()
    # move_count が 0 以下の場合に < ボタンを操作できないようにする
+   prev_button.disabled = self.move_count <= 0
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

上記の修正後に、下記のプログラムで `gui_play` を実行すると、少しわかりづらいですが __< ボタンが薄く表示__ され、__操作できなくなります__。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/db46c9f5-edff-5cac-9888-efcb41d6aaf4.png" width="250">

### 操作が禁止されたボタンのウィジェットの特徴

ipywidgets では、ボタンのウィジェットの `disabled` 属性に `True` を代入すると、上図の < ボタンのように色が薄く表示されるようになります。その場合でも __ボタンの上でマウスを押すと下図のようにボタンの色が変化する__ ので、ボタンを操作できるように見えるかもしれませんが、__ボタンを押してもイベントハンドラは実行されません__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/79297706-f557-5d99-78ba-273cd0b15b58.png" width="250">

そのことは、上図で __いくつかの着手を行った後__ で __< ボタンを押しても 1 手前の局面に戻らない__ ことで確認できます。着手を行った後で < ボタンを押しても 1 手前の局面に戻らない理由は、`play` メソッドの中で、__< ボタンの `disabled` 属性が変化する__ のは、`play` メソッドを実行し、その中で __`restart` メソッドを呼び出した直後だけ__ なので、__着手を行っても `disabled` 属性の値は `True` のまま変化しません__。そのため、着手を行った後でも < ボタンの操作は禁止されたままなので、クリックしてもイベントハンドラは実行されません。もちろん、着手を行った後で < ボタンが禁止されたままなのは変なので、この後で修正します。

### 操作できないボタンを明確にする方法 その 1

ipywidgets でボタンを作成した場合、先程の図のように元々が薄い灰色でボタンが表示されるため、`disabled` 属性に `True` を代入した場合のボタンの表示と、`False` を代入した場合の __ボタンの表示の見分けがつきづらい__ という問題があります。

この問題を解決する方法としては、ボタンのウィジェットの __表示のスタイル__ を表す [__`style`__](https://qiita.com/ysgeso/items/51842efc6193ed456ebc#style) 属性を使って、__ボタンの表示の色を変更する__ という方法があります。

ボタンの色は、ボタンの作成時にキーワード引数 __`style`__ に __`button_color` をキーとする dict を代入することで設定できるので__、下記のプログラムのように、ボタンを作成する `create_button` の仮引数にボタンの色を設定する `color` を追加し、それを使ってボタンを作成するように修正することで、__好きな色のボタンを作成できるようになります__。なお、下記のプログラムでは、仮引数 `color` を、薄い緑色を表す __`"lightgreen"`__ をデフォルト値とするデフォルト引数としたので、`create_button` を呼び出す処理にキーワード引数 `color` を記述しない場合は、薄い緑色のボタンが作成されます。

* __3 行目__：`create_button` にデフォルト値を `"lightgreen"` とする仮引数 `color` を追加する
* __7 行目__：作成するボタンの色を `color` に設定する

```python 
1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          # ボタンを作成するローカル関数を定義する 
 3          def create_button(description, width, color="lightgreen"):
 4              return widgets.Button(
 5                  description=description,
 6                  layout=widgets.Layout(width=f"{width}px"),
 7                  style={"button_color": color},
 8              )
元と同じなので省略
 9
10  Marubatsu.play = play
```

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

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

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step が負の場合は 0 に修正する
            step = max(0, 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 = None if step == 0 else self.records[step - 1]
            # 描画を更新する
            self.draw_board(ax, ai)
        
        def on_first_button_clicked(b):
            change_step(0)

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

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

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

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)

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

    self.restart()
    # move_count が 0 以下の場合に < ボタンを操作できないようにする
    prev_button.disabled = self.move_count <= 0
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        # ボタンを作成するローカル関数を定義する 
-       def create_button(description, width):
+       def create_button(description, width, color="lightgreen"):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
+               style={"button_color": color},
            )
元と同じなので省略

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

上記の修正後 に、下記のプログラムで `gui_play` を実行すると、実行結果のようにボタンの色が薄い緑色で表示されるようになるので、少しわかりやすくなります。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d437f2f5-cbf6-bb41-0216-5686095e5d20.png" width="250">

### 操作できないボタンを明確にする方法 その 2

操作できないボタンをさらに明確にする方法としては、__操作できないボタンの色__ を、__完全に別の色にする__ というものがあります。ボタンのスタイルの個々の情報は、__`style.スタイルの名前`__ に代入されています。この __スタイルの名前__ は、ボタンを作成する際にキーワード引数 `style` に代入した __dict のキーと同じ名前__ です。従って、ボタンの色は、__`.style.button_color`__ に色を表すデータを代入することで変更できます。

下記のプログラムは 6 行目で `disabled` 属性に `True` が代入された場合とそうでない場合で、ボタンの色を薄い灰色と薄い緑色で、__完全に異なる色にする__ 処理を行います。他の配色が良いと思った方は自由に変更して下さい。

```python
1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):     
元と同じなので省略
2      self.restart()
3      # move_count が 0 以下の場合に < ボタンを操作できないようにし、色を変更する
4      prev_button.disabled = self.move_count <= 0
5      prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"
6      return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
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):      
    # 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, color="lightgreen"):
            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 を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step が負の場合は 0 に修正する
            step = max(0, 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 = None if step == 0 else self.records[step - 1]
            # 描画を更新する
            self.draw_board(ax, ai)
        
        def on_first_button_clicked(b):
            change_step(0)

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

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

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

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)

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

    self.restart()
    # move_count が 0 以下の場合に < ボタンを操作できないようにし、色を変更する
    prev_button.disabled = self.move_count <= 0
    prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):     
元と同じなので省略
    self.restart()
    # move_count が 0 以下の場合に < ボタンを操作できないようにし、色を変更する
    prev_button.disabled = self.move_count <= 0
+   prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

上記 の修正後に、下記のプログラムで `gui_play` を実行すると、操作が禁止された < ボタンの色が薄い灰色で表示されるようになるので、さらにわかりやすくなります。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/854375f9-2caa-13aa-e1a0-78f893d98743.png" width="250">

### `update_widgets_status` の定義

先程説明したように、上記のプログラムでは、__< ボタンに対する処理__ を `play` メソッド内の  __`restart` メソッドを呼び出した直度でしか行っていない__ ので、__着手を行っても < ボタンを操作できないまま__ になっています。そのため、__着手を行った場合__ でも __< ボタンに対する処理を記述する__ 必要がありますが、その場合に __行う処理は全く同じ__ なので、その処理を行う下記のような __関数を定義する__ 事にします。

なお、今後 __他のボタンに対しても同様の処理を行う可能性が高い__ ことを考慮して、__すべてのウィジェットの状態をまとめて変更する__ 処理を行う関数として定義する事にします。

__名前__：複数のウィジェット（widgets）の状態（status）をまとめて更新（update）する処理を行うので、__`update_widgets_status`__ とする
__処理__：その時の状況に応じて、ウィジェットの状態を変更する
__入力__：なし
__出力__：なし

下記は __`update_widgets_status`__ の定義です。

```python
def update_widgets_status():
    # move_count が 0 以下の場合に < ボタンを操作できないようにし、色を変更する
    prev_button.disabled = self.move_count <= 0
    prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"
```

この関数内では、`self` と `prev_button` に関する処理を行う必要があるので、[前回の記事](https://qiita.com/ysgeso/items/aa8654c3b5f55a9e08b7#%E3%83%9C%E3%82%BF%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0%E3%81%AE%E5%AE%9A%E7%BE%A9)のボタンを作成する関数と同様の理由で `update_widgets_status` を __`play` メソッドのローカル関数として定義__ します。また、`play` メソッド内で `restart` メソッドを実行した直後と、`on_mouse_down` 内で人間が着手を行った際にこの関数を呼び出すように修正します。

下記はそのように修正を行ったプログラムです。

* __8 ~ 11 行目__：`update_widgets_status` を定義する
* __20 行目__：マウスを押すことで着手を行った場合に、`update_widgets_status` を呼び出す
* __22 行目__：ゲームをリセットした直後に、`update_widgets_status` を呼び出す

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2      # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
 3      if gui:
 4          # %matplotlib widget のマジックコマンドを実行する
 5          get_ipython().run_line_magic('matplotlib', 'widget')
 6
 7          # ウィジェットの状態を更新する        
 8          def update_widgets_status():
 9              # move_count が 0 以下の場合に < ボタンを操作できないようにし、色を変更する
10              prev_button.disabled = self.move_count <= 0
11              prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
元と同じなので省略        
12          # ローカル関数としてイベントハンドラを定義する
13          def on_mouse_down(event):
14              # Axes の上でマウスを押していた場合のみ処理を行う  
15              if event.inaxes and self.status == Marubatsu.PLAYING:
16                  x = math.floor(event.xdata)
17                  y = math.floor(event.ydata)
18                  self.move(x, y)                
19                  self.draw_board(ax, ai)
20                  update_widgets_status()
元と同じなので省略  
21      self.restart()
22      update_widgets_status()
23      return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
24
25  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')

        # ウィジェットの状態を更新する        
        def update_widgets_status():
            # move_count が 0 以下の場合に < ボタンを操作できないようにし、色を変更する
            prev_button.disabled = self.move_count <= 0
            prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
        
        # 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 を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step が負の場合は 0 に修正する
            step = max(0, 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 = None if step == 0 else self.records[step - 1]
            # 描画を更新する
            self.draw_board(ax, ai)
        
        def on_first_button_clicked(b):
            change_step(0)

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

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

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

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)
                update_widgets_status()

                # 次の手番の処理を行うメソッドを呼び出す
                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()
    update_widgets_status()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

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

        # ウィジェットの状態を更新する        
+       def update_widgets_status():
            # move_count が 0 以下の場合に < ボタンを操作できないようにし、色を変更する
+           prev_button.disabled = self.move_count <= 0
+           prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
元と同じなので省略        
        # ローカル関数としてイベントハンドラを定義する
        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)
+               update_widgets_status()
元と同じなので省略
    self.restart()
-   prev_button.disabled = self.move_count <= 0
-   prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"
+   update_widgets_status()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

上記の修正後に、下記 のプログラムで `gui_play` を実行して着手を行うと、実行結果のように < ボタンの色が緑色に変化し、操作できるようになります。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/5c655db1-778e-94c5-926a-f119d9cffd5a.png" width="250">

上記のプログラムには __< ボタンに関する問題がいくつかあります__。上記の後で様々な操作を行い、< ボタンの表示を観察してどのような問題があるかを少し考えてみて下さい。

## < ボタンに関する問題と修正

< ボタンに関する問題は以下の通りです。

* いくつかの着手を行った後で、__< ボタンをクリック__ して __ゲーム開始時の局面__ にしても、< ボタンの表示が __灰色にならない__
* __リセットボタンをクリック__ して __ゲーム開始時の局面__ にしても、< ボタンの表示が __灰色にならない__

上記では、__&lt; ボタンのみに言及__ していますが、残りの <、>、>> ボタンをクリックして __手数が変化した場合__ でも __< ボタンの状態を更新する必要があります__。リプレイに関するこれらのボタンをクリックした際には、__`change_step`__ が呼び出されるので、__`change_step` 内で `update_widgets_status()` を呼び出す__ ようにすることで、手数が変化した際に、その時の状況にあわせて < ボタンの表示が変化するようになります。

2 つ目の問題に関しては、リセットボタンがクリックされた際のイベントハンドラ内で `update_widgets_status()` を呼び出すことで問題を解決できます。

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

* __5、13 行目__：リセットボタンがクリックされた時と、リプレイ機能のボタンがクリックされた時に `update_widgets_status` を呼び出す

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          # リセットボタンのイベントハンドラを定義する
 3          def on_reset_button_clicked(b):
 4              self.restart()
 5              update_widgets_status()
 6              on_change_button_clicked(b)
元と同じなので省略
 7          def change_step(step):
 8              # step が負の場合は 0 に修正する
 9              step = max(0, step)
元と同じなので省略
10              # 描画を更新する
11              self.draw_board(ax, ai)
12              # ウィジェットの状態を更新する
13              update_widgets_status()
元と同じなので省略
14
15  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')

        # ウィジェットの状態を更新する        
        def update_widgets_status():
            # move_count が 0 以下の場合に < ボタンを操作できないようにし、色を変更する
            prev_button.disabled = self.move_count <= 0
            prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
        
        # 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()
            update_widgets_status()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step が負の場合は 0 に修正する
            step = max(0, 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 = None if step == 0 else self.records[step - 1]
            # 描画を更新する
            self.draw_board(ax, ai)
            # ウィジェットの状態を更新する
            update_widgets_status()
        
        def on_first_button_clicked(b):
            change_step(0)

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

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

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

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)
                update_widgets_status()

                # 次の手番の処理を行うメソッドを呼び出す
                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()
    update_widgets_status()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
+           update_widgets_status()
            on_change_button_clicked(b)
元と同じなので省略
        def change_step(step):
            # step が負の場合は 0 に修正する
            step = max(0, step)
元と同じなので省略
            # 描画を更新する
            self.draw_board(ax, ai)
            # ウィジェットの状態を更新する
+           update_widgets_status()
元と同じなので省略

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

実行結果は省略しますが、上記の修正後に、下記 のプログラムで `gui_play` を実行し、様々な操作を行うことで、0 手目の局面では、必ず < ボタンが灰色になり、クリックしても何も起きなります。実際に確認してみて下さい。

```python
gui_play()
```

## 2 つの修正方法の併用

今回の記事では < ボタンに関する問題を修正する方法として、下記の 2 つを紹介しました。

* < ボタンの操作によって、__負の手数にならないようにする__
* 手数が 0 以下の時に __< ボタンを操作できないようにする__

上記の __いずれか片方の修正だけ__ で、< ボタンに関する __問題を修正することができます__ が、この 2 つの修正は __両方とも行ったほうが良い__ と思います。

同じことを 2 度行うのは無駄ですが、この 2 つは __異なる観点で問題を修正__ しているのでいわゆる 2 重のチェックになり、__より確実に問題を修正__ することができます。

例えば、片方のチェックに引っかからないような問題が発生したとしても、もう一つの別のチェックに引っかかることで問題が修正される可能性が高まるからです。

# << ボタンに関する修正

最初に列挙したリプレイ機能の問題点では取り上げませんでしたが、__<< ボタンの表示__ が気になっている方が多いのではないかと思いますので、修正を行います。

__<< ボタン__ は、__ゲーム開始時の 0 手目の局面に移動するボタン__ なので、0 手目の局面で << ボタンを押しても、0 が代入されている `move_count` 属性に 0 を代入するという、__実質的に何も行わない処理が行われる__ だけで、エラーは発生しません。従って 0 手目の局面で、<< ボタンを操作できないようにしなくても、エラーが発生するようなことはありません。

しかし、エラーが発生しなくても、0 手目の局面で、下図のように << ボタンは操作できるが、< ボタンは操作できないような表示は、__見た目が不自然__ です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/854375f9-2caa-13aa-e1a0-78f893d98743.png" width="250">

また、0 手目の局面で << ボタンを操作できないようにしても大きな問題は発生しないので、__<< ボタンも__ < ボタンと同様に、下記のプログラムのように、__0 手目の局面では灰色で表示して操作できないようにする__ ことにします。

* __7、8 行目__：<< ボタンも < ボタンと同様に 0 手目以下の場合に操作できないようにする

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          # ウィジェットの状態を更新する        
 3          def update_widgets_status():
 4              # move_count が 0 以下の場合に < ボタンと << ボタンを操作できないようにし、色を変更する
 5              prev_button.disabled = self.move_count <= 0
 6              prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
 7              first_button.disabled = self.move_count <= 0
 8              first_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
元と同じなので省略        
 9
10  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')

        # ウィジェットの状態を更新する        
        def update_widgets_status():
            # move_count が 0 以下の場合に < ボタンと << ボタンを操作できないようにし、色を変更する
            prev_button.disabled = self.move_count <= 0
            prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
            first_button.disabled = self.move_count <= 0
            first_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
        
        # 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()
            update_widgets_status()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step が負の場合は 0 に修正する
            step = max(0, 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 = None if step == 0 else self.records[step - 1]
            # 描画を更新する
            self.draw_board(ax, ai)
            # ウィジェットの状態を更新する
            update_widgets_status()
        
        def on_first_button_clicked(b):
            change_step(0)

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

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

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

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)
                update_widgets_status()

                # 次の手番の処理を行うメソッドを呼び出す
                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()
    update_widgets_status()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        # ウィジェットの状態を更新する        
        def update_widgets_status():
            # move_count が 0 以下の場合に < ボタンと << ボタンを操作できないようにし、色を変更する
            prev_button.disabled = self.move_count <= 0
            prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
+           first_button.disabled = self.move_count <= 0
+           first_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
元と同じなので省略        

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

上記 の 修正後 に、下記 のプログラムで `gui_play` を 実行 すると、実行結果のように、ゲーム開始時の 0 手目の局面では必ず < ボタンと << ボタンが灰色になり、クリックしても何も起きなくなることが確認できます。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d3958831-0e24-3cdd-6704-bc409e3f8534.png" width="250">

## ボタンの状態を変更する関数の定義

上記のプログラムでは、ボタンの状態を変更する処理を、下記のプログラムのように、ボタンごとに 2 行ずつ記述していましたが、__この処理は共通する処理で__ す。

```python
        # ウィジェットの状態を更新する        
        def update_widgets_status():
            # それぞれのボタンに対して操作できるかどうかを設定し、色を変更する
            prev_button.disabled = self.move_count <= 0
            prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
            first_button.disabled = self.move_count <= 0
            first_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"             
```

そこで、ボタンの状態を変更する処理を行う、下記のような関数を定義することにします。

__名前__：ボタンの状態（status）を設定するので、__`set_button_status`__ とする
__処理__：指定したボタンのウィジェットを指定した状態に変更する
__入力__：仮引数 __`button`__ にボタンのウィジェットを、__`disabled`__ にボタンの状態を代入する
__出力__：なし

この関数を、下記のプログラムのように `play` メソッドのローカル関数として定義し、`update_widgets_status`内で呼び出して利用するように修正します。

* __8 ~ 10 行目__：ボタンのウィジェットの状態を設定する `set_button_status` を定義する
* __15、16 行目__：`set_button_status` を利用して、<< と < ボタンの状態を更新する

```python
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2      # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
 3      if gui:
 4          # %matplotlib widget のマジックコマンドを実行する
 5          get_ipython().run_line_magic('matplotlib', 'widget')
 6
 7          # ボタンのウィジェットの状態を設定する
 8          def set_button_status(button, disabled):
 9              button.disabled = disabled
10              button.style.button_color = "lightgray" if disabled else "lightgreen"
11
12          # ウィジェットの状態を更新する        
13          def update_widgets_status():
14              # 0 手目以下の場合に、<< と < ボタンを操作できないようにする
15              set_button_status(first_button, self.move_count <= 0)
16              set_button_status(prev_button, self.move_count <= 0)
元と同じなので省略
17
18  Marubatsu.play = play
```

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

```python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')

        # ボタンのウィジェットの状態を設定する
        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)
        
        # 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()
            update_widgets_status()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step が負の場合は 0 に修正する
            step = max(0, 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 = None if step == 0 else self.records[step - 1]
            # 描画を更新する
            self.draw_board(ax, ai)
            # ウィジェットの状態を更新する
            update_widgets_status()
        
        def on_first_button_clicked(b):
            change_step(0)

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

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

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

        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)
                update_widgets_status()

                # 次の手番の処理を行うメソッドを呼び出す
                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()
    update_widgets_status()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

```diff_python
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
    # 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 手目以下の場合に、<< と < ボタンを操作できないようにする
-           prev_button.disabled = self.move_count <= 0
-           prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
-           first_button.disabled = self.move_count <= 0
-           first_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"  
+           set_button_status(first_button, self.move_count <= 0)
+           set_button_status(prev_button, self.move_count <= 0)
元と同じなので省略

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

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

```python
gui_play()
```

# 今回の記事のまとめ

今回の記事では、リプレイ機能の __< ボタンの処理に関する問題を修正__ しました。また、__操作してはいけない状況__ で、<< と < ボタンを __操作できないように修正__ しました。

次回の記事では、残りの問題を修正します。

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

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

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

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

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

以下のリンクは、今回の記事で作成した util.py です。

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

# 次回の記事

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

[^1]:disabled は、何かをできなくするという意味の英単語です