Pythonで〇×ゲームのAIを一から作成する　その80　リプレイ機能の残りの問題の修正

# 目次と前回の記事

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

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

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

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

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

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

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

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

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

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

# リプレイ機能の問題点（再掲）

前回の記事では、下記のリプレイ機能の問題点のうち、最初の 2 つを修正しました。今回の記事では残りの問題点について修正します。

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

# 最後の着手が行われた局面で >ボタンをクリックするとエラーが発生する問題点の修正

下記のプログラムで `gui_play` を実行し、__最後の着手が行われた局面__ で __> ボタンをクリックする__ と下記のような __エラーが発生__ します。このエラーを発生させるには、ゲーム開始時に > ボタンをクリックするのが簡単です。このエラーの原因ついて少し考えてみて下さい。

```python
from util import gui_play

gui_play()
```

実行結果

```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
File c:\Users\ys\ai\marubatsu\080\marubatsu.py:420, in Marubatsu.play.<locals>.on_next_button_clicked(b)
    419 def on_next_button_clicked(b):
--> 420     change_step(self.move_count + 1)

File c:\Users\ys\ai\marubatsu\080\marubatsu.py:399, in Marubatsu.play.<locals>.change_step(step)
    397 step = max(0, step)
    398 # step 手目のゲーム盤のデータをコピーし、board に代入する
--> 399 self.board = deepcopy(self.board_records[step])
    400 # 手数を表す step を move_count に代入する
    401 self.move_count = step

IndexError: list index out of range
```

## エラーの原因の検証

このエラーの原因は、上記のエラーメッセージから、__`self.board_records[step]`__ で、__`step`__ が `board_records` に代入された __list の範囲外のインデックス__ であることです。実際に、最後の着手が行われた局面で > ボタンをクリックすると __`step`__ には __最後の着手の局面の次の要素のインデックス__ が代入されますが、__`board_records`__ には __そのような要素は存在しません__。

このエラーを修正するためには、[前回の記事](https://qiita.com/ysgeso/items/94fba56cb97dc4787ca8#-%E3%83%9C%E3%82%BF%E3%83%B3%E3%82%92%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%81%99%E3%82%8B%E3%81%A8%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%8C%E7%99%BA%E7%94%9F%E3%81%99%E3%82%8B%E5%8E%9F%E5%9B%A0)で < ボタンに対して同じ list index out of range のエラーが発生した際に行った修正と同様に、下記のプログラムの 4 行目のように、__`step`__ が __`self.board_records` の要素の数以上になった場合__ に、__`step`__ に`self.board_records` の __最後のインデックスの値を代入__ するように修正する必要があります。

```python
1  def change_step(step):
2      # step がインデックスの範囲外の場合は、範囲内に収める
3      step = max(0, step)
4      step = min(len(self.board_records) - 1, step)
元と同じなので略
```

なお、上記のプログラムの 3、4 行目は、下記のプログラムの 3 行目のように 1 行でまとめることもできますが、わかりづらいと思った方は上記のプログラムを採用して下さい。

```python
1  def change_step(step):
2      # step がインデックスの範囲外の場合は、範囲内に収める
3      step = max(0, min(len(self.board_records) - 1, step))
元と同じなので略
```

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

```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):      
元と同じなので省略
        def change_step(step):
            # step がインデックスの範囲外の場合は、範囲内に収める
            step = max(0, min(len(self.board_records) - 1, step))
元と同じなので省略

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')

        # ボタンのウィジェットの状態を設定する
        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 がインデックスの範囲外の場合は、範囲内に収める
            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 = 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 change_step(step):
            # step がインデックスの範囲外の場合は、範囲内に収める
-           step = max(0, step)
+           step = max(0, min(len(self.board_records) - 1, step))
元と同じなので省略

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

実行結果は省略しますが、上記の修正後に、下記のプログラムで `gui_play` を実行すると、> ボタンをクリックしてもエラーが発生しなくなることが確認できます。

```python
gui_play()
```

## > と >> ボタンの表示の変更

<< ボタンや < ボタンと同様に、<strong>> と >> ボタン</strong> も、__最後の着手が行われた局面__ で __操作をできないように修正する__ ことにします。それらのボタンが __操作を行えない条件__ は、__`move_count`__ 属性が __`self.board_records`__ に代入された __list の要素の数 - 1 以上__ の場合なので、[`set_button_status`](https://qiita.com/ysgeso/items/94fba56cb97dc4787ca8#%E3%83%9C%E3%82%BF%E3%83%B3%E3%81%AE%E7%8A%B6%E6%85%8B%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0%E3%81%AE%E5%AE%9A%E7%BE%A9) を使って下記のプログラムのように `play` メソッドを修正します。

* __7、8 行目__：> と >> ボタンに対して、`move_count` 属性が最後の着手が行われた手数以上の場合に、操作できないようにする

```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              # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
 5              set_button_status(first_button, self.move_count <= 0)
 6              set_button_status(prev_button, self.move_count <= 0)
 7              set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
 8              set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
元と同じなので省略
 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 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)
        
        # 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 がインデックスの範囲外の場合は、範囲内に収める
            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 = 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():
            # 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)
元と同じなので省略

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

上記の修正後に、下記のプログラムで `gui_play` を実行すると、実行結果のように、ゲーム開始時の局面ではすべてのリプレイのボタンが灰色になり、クリックしても何も起きなくなることが確認できます。また、着手を行い、リプレイのボタンをクリックすると、手数に応じて適切にリプレイのボタンの表示が変化することを確認して下さい。

```python
gui_play()
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b14de39a-f8fa-4af6-b76c-489bd23ab44a.png" width="250">

# 最後の着手が行われた以外の局面で着手を行うとおかしな挙動が発生する問題点の修正

__最後の着手が行われた以外の局面__ で空いているマスをクリックして __着手を行う__ と __おかしな挙動が発生__ します。わかりづらいと思いますので、具体例を示します。

| | 操作 | 表示 |
|:-:|:--|:-:|
| 1 | (0, 0)、(1, 0)、(2, 0) の順で着手を行う  | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/125d9827-5da3-4da1-e744-21f94d8746da.png" width="200">  |
| 2 | < ボタンをクリックして 2 手目の局面に戻す  | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b28df9cb-519f-3c6a-78fc-39afdbdc3ed3.png" width="200">  |
| 3 | (0, 1) のマスをクリックして着手を行う  |  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b07e0bba-4f72-2888-d11a-399b4b4a1589.png" width="200"> |
| 4 | > ボタンをクリックして次の手数の局面を表示する  | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b07e0bba-4f72-2888-d11a-399b4b4a1589.png" width="200">  |
| 5 | < ボタンをクリックして前の手数の局面を表示する  | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/125d9827-5da3-4da1-e744-21f94d8746da.png" width="200">  |

上記で行われたおかしな挙動は以下の通りです。

* 操作 4 で、__次の手数の局面を表示__ した際に、__表示内容が変化しない__
* 操作 5 で、__前の手数の局面を表示__ した場合に、操作 3 の 3 手目の局面ではなく、__操作 1 の 3 手目の局面が表示される__

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

## 問題の検証

問題を検証するために、__それぞれの操作__ で、__`board`__、__`board_records`__、__`move_count`__ のそれぞれに代入された __データを検証__ することにします。

### ゲーム開始時の処理の検証

__ゲーム開始時__ には、下記の __`restart`__ メソッドの処理が行われます。

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

上記の 2、4、9 行目の処理によって、それぞれの変数には下記の表のデータが代入されます。なお、わかりやすさを重視して、`board` と `board_records` の __要素のデータ__ には、__ゲーム盤の画像を表記__ します。


| 変数 | 値 |
|:--|--:|
| `board` | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70"> |
| `board_records`  | [ <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70"> ] |
| `move_count`  | 0  |

:::note
本当にそれぞれの変数の値が上記のような値になっているかどうかを確認したい人は、`restart` メソッド内に `print` を記述して変数の値を表示すると良いでしょう。
:::

### 操作 1 の検証

__操作 1__ では、__3 手分の着手__ が下記の __`move`__ メソッドで行われます。

```python
 1  def move(self, x, y):
 2      if self.place_mark(x, y, self.turn):
 3          self.last_turn = self.turn
 4          self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
 5          self.move_count += 1
 6          self.status = self.judge()
 7          self.last_move = x, y
 8          self.records.append(self.last_move)
 9          self.board_records.append(deepcopy(self.board))
```

`move` メソッドでは以下の処理が行われます。

* 2 行目の `place_mark` メソッドの呼び出しで着手を `board` 属性のデータに反映させる
* 5 行目で `move_count` に 1 を足す
* 9 行目で `board_records` の要素に `board` 属性のデータをコピーしたデータを追加する

従って、操作 1 によって それぞれの変数には下記のデータが代入されます。

| 変数 | 値 |
|:--|--:|
| `board`<br>（3 手目の局面になる） | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a13de375-fd10-8c11-2d17-8e72254fbbb5.png" width="70"> |
| `board_records`<br>（0 ~ 3 手目の局面が代入される）  | [ <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3eee9721-b553-46be-7794-d1d247b21063.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/87bada89-be66-1f7b-e356-228e318cc76e.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a13de375-fd10-8c11-2d17-8e72254fbbb5.png" width="70"> ] |
| `move_count`  | 3  |

### 操作 2 の検証

__< ボタンをクリック__ すると、下記の __`on_prev_button_clicked`__ が呼び出されます。

```python
 1  def on_prev_button_clicked(b):
 2      change_step(self.move_count - 1)
 3 
 4  def change_step(step):
 5      step = max(0, step)
 6      self.board = deepcopy(self.board_records[step])
 7      self.move_count = step
 8      self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
 9      self.status = self.judge()
10      self.last_move = None if step == 0 else self.records[step - 1]
11      self.draw_board(ax, ai)
12      update_widgets_status()
```

上記の __`on_prev_button_clicked`__ では以下の処理が行われます。下記の処理では、__`board_records` 属性の値__ は __変化しない__ 点に注意して下さい。

* 4 行目で 仮引数 `step` に `self.move_count - 1`、すなわち 3 - 1 = 2 が代入される
* 6 行目で `board` 属性に `self.board_records[2]` をコピーしたものが代入される
* 7 行目で `move_count` 属性に 2 が代入される

従って、__操作 2__ によってそれぞれの変数には下記のデータが代入されます。

| 変数 | 値 |
|:--|--:|
| `board`<br>（2 手目の局面になる） | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/87bada89-be66-1f7b-e356-228e318cc76e.png" width="70"> |
| `board_records`<br>（変化しない）  | [ <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3eee9721-b553-46be-7794-d1d247b21063.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/87bada89-be66-1f7b-e356-228e318cc76e.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a13de375-fd10-8c11-2d17-8e72254fbbb5.png" width="70"> ] |
| `move_count`  | 2  |

### 操作 3 の検証

__(0, 1)__ に着手を行う __操作 3__ では、下記の __`move`__ メソッドで下記の処理が行われます。

```python
 1  def move(self, x, y):
 2      if self.place_mark(x, y, self.turn):
 3          self.last_turn = self.turn
 4          self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
 5          self.move_count += 1
 6          self.status = self.judge()
 7          self.last_move = x, y
 8          self.records.append(self.last_move)
 9          self.board_records.append(deepcopy(self.board))
```

* 2 行目で (0, 1) への着手を `board` 属性のデータに反映させる
* 5 行目で `move_count` に 1 を足して、2 + 1 = 3 を代入する
* 9 行目で `board_records` の要素に `board` 属性のデータをコピーしたデータを追加する

従って、操作 3 によってそれぞれの変数には下記のデータが代入されます。

| 変数 | 値 |
|:--|--:|
| `board` | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d0dbc928-2314-afd0-95a8-b8b820476c57.png" width="70"> |
| `board_records`  | [ <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3eee9721-b553-46be-7794-d1d247b21063.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/87bada89-be66-1f7b-e356-228e318cc76e.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a13de375-fd10-8c11-2d17-8e72254fbbb5.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d0dbc928-2314-afd0-95a8-b8b820476c57.png" width="70"> ] |
| `move_count`  | 3 |

__操作 3__ によって、__`board`__ 属性には __2 手目の局面__ で、__(0, 1) に着手を行ったデータ__ が代入されるので、__正しい画像が描画__ されます。

しかし、__`board_records`__ の __内容__ は元の 0 ~ 3 手目の局面のデータに __新しい局面のデータを追加__ した、__おかしなデータ__ になります。__本当__ は、__`board_records`__ は下記のように、__元の 3 手目のデータ__ を __新しい局面のデータで入れ替えたもの__ になってほしいのですが、__`move`__ メソッドが __`board_records` に対して行う処理__ は、__新しい局面のデータを追加__ するという処理なので、__下記のようなデータにはなりません__。

[ <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3eee9721-b553-46be-7794-d1d247b21063.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/87bada89-be66-1f7b-e356-228e318cc76e.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d0dbc928-2314-afd0-95a8-b8b820476c57.png" width="70"> ] 

また、__`board_records[3]`__ には、下図左の __操作 1 による 3 手目__ の __古い__ ゲーム盤のデータが代入されており、__実際に画面に描画__ される __`board`__ 属性の、下図右の __操作 3 による 3 手目__ の __新しい__ ゲーム盤のデータとは __異なる__ という、__食い違った状況が発生__ しています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a13de375-fd10-8c11-2d17-8e72254fbbb5.png" width="70">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d0dbc928-2314-afd0-95a8-b8b820476c57.png" width="70">

このように、この後の操作 4、5 で行われるおかしな挙動は、実は __操作 3 の時点__ で __`board_records`__ が __おかしなデータになっている__ ことが原因です。

### 操作 4 の検証

プログラムの表記は省略しますが、__操作 4__ では、__`on_next_button_clicked`__ が呼び出され、以下の処理が行われます。__操作 2 と同様__ に __`board_records`__ 属性の値は __変化しません__。

* `step` に `self.move_count + 1`、すなわち 3 + 1 = 4 が代入される
* `board` 属性に `self.board_records[4]` をコピーしたものが代入される
* `move_count` 属性に 4 が代入される

従って、操作 4 によってそれぞれの変数には下記のデータが代入されます。

| 変数 | 値 |
|:--|--:|
| `board` | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d0dbc928-2314-afd0-95a8-b8b820476c57.png" width="70"> |
| `board_records`<br>（変化しない）  | [ <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3eee9721-b553-46be-7794-d1d247b21063.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/87bada89-be66-1f7b-e356-228e318cc76e.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a13de375-fd10-8c11-2d17-8e72254fbbb5.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d0dbc928-2314-afd0-95a8-b8b820476c57.png" width="70"> ] |
| `move_count`  | 4 |

__操作 4__ によって、描画される __ゲーム盤の画像が変化しない__ のは、先程説明したように、__`board_records[4]`__ に代入されている __ゲーム盤のデータ__ が、__操作 3__ によって __`board_records`__ に __追加されたゲーム盤のデータと同じ__ だからです。

### 操作 5 の検証

プログラムの表記は省略しますが、__操作 5__ では、__`on_prev_button_clicked`__ が呼び出され、以下の処理が行われます。

* `step` に `self.move_count - 1`、すなわち 4 - 1 = 3 が代入される
* `board` 属性に `self.board_records[3]` をコピーしたものが代入される
* `move_count` 属性に 3 が代入される

従って、__操作 5__ によってそれぞれの変数には下記のデータが代入されます。

| 変数 | 値 |
|:--|--:|
| `board` | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a13de375-fd10-8c11-2d17-8e72254fbbb5.png" width="70"> |
| `board_records`<br>（変化しない）  | [ <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3eee9721-b553-46be-7794-d1d247b21063.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/87bada89-be66-1f7b-e356-228e318cc76e.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a13de375-fd10-8c11-2d17-8e72254fbbb5.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d0dbc928-2314-afd0-95a8-b8b820476c57.png" width="70"> ] |
| `move_count`  | 3 |

__操作 3 の後__ で、__> と < の順__ でボタンをクリックした際に、__操作 3__ の時とは __異なるゲーム盤が描画される__ のは、以下のような理由からです。

* 操作 3 では、操作 2 の局面に対して (0, 1) に着手を行ったゲーム盤が描画される
* 操作 5 では、操作 1 で行った 3 手目の局面を表す `board_records[3]` のゲーム盤のデータが描画される

おかしな挙動が行われる原因がわかったので、問題の修正方法を少し考えてみて下さい。

## 問題の修正

この問題が発生する原因は、__`move`__ メソッドで __行う処理__ が、常に __`board_records`__ に `board` 属性の値をコピーしたデータを __追加する処理__ だからです。__最後の着手を行った局面__ であれば __その処理で問題はありません__ が、__それ以外の局面__ の場合は、__`board_records`__ の中の、行った着手に対応する要素とは __異なる要素に `board` 属性の値が代入__ されてしまいます。

__`move`__ メソッドで行う __着手に対応__ する __`board_records`__ の __要素のインデックス__ は、__手数を表す `move_count` 属性の値に等しい__ ので、`move` メソッドで `board_records` に対して行う処理を、下記のプログラムの 9 行目のように、__`board_records[self.move_count]`__ に `board` 属性の値をコピーしたデータを代入するように修正することで、この問題を解決できると思った人がいるかもしれません。

```python
 1  def move(self, x, y):
 2      if self.place_mark(x, y, self.turn):
 3          self.last_turn = self.turn
 4          self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
 5          self.move_count += 1
 6          self.status = self.judge()
 7          self.last_move = x, y
 8          self.records.append(self.last_move)
 9          self.board_records[self.move_count] = deepcopy(self.board)
10
11  Marubatsu.move = move
```

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

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

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

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

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

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

しかし、__上記の修正方法__ には __いくつかの問題があります__。どのような問題があるかについて少し考えてみて下さい。

### 問題の検証と修正

上記の修正後に、下記のプログラムで `gui_play` を実行後に、好きなマスをクリックして着手を行うと、実行結果のようなエラーが発生します。

```python
gui_play()
```

実行結果

```
略
Cell In[6], line 9
      7 self.last_move = x, y
      8 self.records.append(self.last_move)
----> 9 self.board_records[self.move_count] = deepcopy(self.board)

IndexError: list assignment index out of range
```

実行結果から、__`self.board_records[self.move_count] = deepcopy(self.board)`__ の処理で、__範囲外のインデックスを指定__ したことによって __エラーが発生する__ ことがわかります。

__1 手目の着手を行う__ と、__`move_count`__ 属性には __`1` が代入__ されますが、__`board_records`__ にはゲーム開始時の局面を表す 0 手目のゲーム盤のデータしか代入されておらず、__1 番の要素に対応するデータは存在しません__。そのためこのエラーが発生します。

従って、このエラーは、下記のプログラムのように、__`board_records` の要素の数__ が手数を表す __`move_count` 未満の場合__ は元のプログラムと同様に __`append` を使って要素を追加__ し、__そうでなければ__ 上記のように __代入を行う__ ようにすることで修正することができます。

```python
1  def move(self, x, y):
元と同じなので省略
2          self.records.append(self.last_move)
3          if len(self.board_records) <= self.move_count:            
4              self.board_records.append(deepcopy(self.board))
5          else:
6              self.board_records[self.move_count] = deepcopy(self.board)
7
8  Marubatsu.move = move
```

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

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

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

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

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

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

上記の修正を行った後で、下記のプログラムを実行して先程の操作 1 ~ 5 を行って下さい。

```python
gui_play()
```

操作 3 で、(0, 1) に着手を行うと、下図のように、__> と >>__ ボタンが __灰色で表示__ され、操作できなくなります。__3 手目が最後の着手__ なので、これは __正しい動作__ です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/13c84657-2d82-9896-b705-79f5f234db84.png" width="200">

これで問題は解決できたと思う人がいるかもしれませんが、実は別のバグが潜んでいます。それは、上記の後に __<、> の順でボタンをクリック__ すると、下図のように __赤いマークが表示されなくなる__ というものです。その原因について少し考えてみて下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/947cc692-b500-e97e-f2fc-83c94e6086fb.png" width="200">

### 直前の着手が赤色で表示されなくなる原因の検証

__赤いマーク__ は __直前の着手を表す__ ので、__原因__ は直前の着手の座標を表す __`last_move`__ 属性と、直前の着手を記録した __`records`__ 属性に __あるはず__ です。先程と同様の方法で、それぞれの操作によって、`last_move` と `records` 属性の値がどのように変化するかを検証することで問題の原因を見つけることができますが、同じような検証をもう一度詳しく記述するのは冗長なので、結論だけを記します。興味がある方は先ほどと同様の方法で検証してみて下さい。

__`records`__ 属性は、__`board_records` 属性と同様__ に、__`move`__ メソッドで __着手を行うたび__ にその __着手のデータを list で記録__ します。従って、__`move`__ メソッドでは __`records`__ 属性に対して __`board_records` と同様の処理__ を行う必要がありますが、先程 __`board_records`__ 属性に対する処理を、下記のプログラムの __3 ~ 6 行目のように修正した際__ に、__`records`__ 属性に対する __2 行目の処理を修正しませんでした__。これがバグの原因です。

```python
1  def move(self, x, y):
元と同じなので省略
2          self.records.append(self.last_move)
3          if len(self.board_records) <= self.move_count:            
4              self.board_records.append(deepcopy(self.board))
5          else:
6              self.board_records[self.move_count] = deepcopy(self.board)
7
8  Marubatsu.move = move
```

### 直前の着手が赤色で表示されなくなる問題の修正

下記は、`records` 属性に対しても同様の処理を行うように修正したプログラムです。なお、このような修正のし忘れはよくあることです。実際に筆者もこの修正を行うのをうっかり忘れてしまい、後でおかしな挙動が発生した際に気づいて修正しました。

* __4、7 行目__：`board_records` と同じ方法で、最後に行われた着手のデータを記録する

```python
1  def move(self, x, y):
元と同じなので省略
2          if len(self.board_records) <= self.move_count:            
3              self.board_records.append(deepcopy(self.board))
4              self.records.append(self.last_move)
5          else:
6              self.board_records[self.move_count] = deepcopy(self.board)
7              self.records[self.move_count] = self.last_move
8
9  Marubatsu.move = move
```

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

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

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

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

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

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

上記の修正後に、下記のプログラムで `gui_play` を実行し、先程の手順で操作を行うと、手順 2 で < ボタンをクリックした後の __手順 3 で (0, 1) のマスに着手を行う__ と実行結果のような __エラーが発生__ してしまいます。このエラーの原因について少し考えててみて下さい。

```python
gui_play()
```

実行結果

```
略
Cell In[10], line 13
     11 else:
     12     self.board_records[self.move_count] = deepcopy(self.board)
---> 13     self.records[self.move_count] = self.last_move

IndexError: list assignment index out of range
```

### 新たなエラーの原因の検証と修正

エラーメッセージから、__`self.records[self.move_count]`__ の __`self.move_count`__ が、__list のインデックスの範囲外__ であることがわかります。初心者の方にはこの原因を見つけづらいのではないかと思いますが、このエラーの原因は、__`board_records`__ 属性が __0 手目 のゲーム開始時の局面から__ のゲーム盤のデータを __記録している__ のに対し、__`records`__ 属性は、__1 手目から__ の着手のデータを __記録している__ からです。そのため、__`records`__ 属性の __list の要素の数__ は、__`board_records`__ 属性の __list の要素の数より__ も __1 つだけ小さくなります__。

:::note
このことに気づく方法の一つは、先程行ったように、操作ごとの `records` 属性の値を検証するというものですが、紙に書いて検証するのが面倒な場合は、`move` メソッドや、`change_step` の中に `print(self.records)` を記述して、操作を行うたびに `records` 属性の値を表示すると良いでしょう。

また、`print` を書いたり消したりするのが面倒な場合は、[`test_judge`](https://qiita.com/ysgeso/items/474db9ea1805993eaa34#%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E8%A1%A8%E7%A4%BA%E3%81%AE%E5%AE%9F%E8%A3%85)で実装したような、デバッグ表示の機能を実装するという方法もあります。
:::

従って、下記のプログラムの 8 行目のように、__`records` のインデックス__ に __`self.move_count - 1` を記述する__ ことで、この問題を修正することができます。

```python
 1  def move(self, x, y):
 2      if self.place_mark(x, y, self.turn):
元と同じなので省略
 3          if len(self.board_records) <= self.move_count:            
 4              self.board_records.append(deepcopy(self.board))
 5              self.records.append(self.last_move)
 6          else:
 7              self.board_records[self.move_count] = deepcopy(self.board)
 8              self.records[self.move_count - 1] = self.last_move
 9
10  Marubatsu.move = move
```

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

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

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

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

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

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

実行結果は省略しますが、上記の修正を行った後で、下記のプログラムを実行して先程の操作を行い、問題が修正されたことを確認して下さい。

```python
gui_play()
```

### データの記録方法の統一による修正方法

上記の修正では、__`records`__ 属性と、__`board_records`__ 属性で、__インデックスの意味が異なる__ ため、__同じ手数を表すの要素__ のデータを __更新する際__ に、下記のプログラムのように __異なるインデックスを計算する必要__ が生じています。このようなプログラムは __わかりづらく__、先程のような __バグの原因となる可能性が高い__ ため __避けたほうが良い__ でしょう。

```python
self.board_records[self.move_count] = deepcopy(self.board)
self.records[self.move_count - 1] = self.last_move
```            

そこで、、__`records`__ 属性と、__`board_records`__ 属性の __インデックスの意味を統一する__ ことにします。具体的には __`records`__ 属性の __インデックス__ を、`board_records` 属性と同様に、ゲーム開始時の __0 手目から数えた手数に対応させる__ ことにします。下記は、そのように `restart` メソッドを修正したプログラムです。
* __2 行目__：`records` 属性を、0 手目のゲーム開始時の局面の直前の着手を表す `self.last_turn` を要素として持つ list で初期化するように修正する

```python
1  def restart(self):
元と同じなので省略
2      self.records = [self.last_turn]
3      self.board_records = [deepcopy(self.board)]  
4
5  Marubatsu.restart = restart        
```

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

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

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

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

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

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

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

```python
 1  def move(self, x, y):
 2      if self.place_mark(x, y, self.turn):
元と同じなので省略
 3          if len(self.board_records) <= self.move_count:            
 4              self.board_records.append(deepcopy(self.board))
 5              self.records.append(self.last_move)
 6          else:
 7              self.board_records[self.move_count] = deepcopy(self.board)
 8              self.records[self.move_count] = self.last_move
 9
10  Marubatsu.move = move
```

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

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

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

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

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

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

これで修正は完了したと思っている人がいるかもしれませんが、__もう一か所修正する必要があります__。それが何かを少し考えてみて下さい。

__`last_move`__ 属性の値は、__リプレイ機能のボタンをクリックした際__ に呼び出される __`change_step` 内で変更される__ ので、その部分を下記のプログラムのように修正する必要があります。なお、__0 手目__ の直前の着手のデータが __`records` 属性内に記録される__ ようになったので、下記のプログラムの 4 行目のように __`last_move` を変更する処理を簡潔に記述できる__ ようになります。修正前のプログラムと比較してみて下さい。

```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              # 直前の着手を計算する
4              self.last_move = self.records[step]
元と同じなので省略
5
6  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)
            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)
        
        # 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 がインデックスの範囲外の場合は、範囲内に収める
            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)
            # ウィジェットの状態を更新する
            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 change_step(step):
元と同じなので省略
            # 直前の着手を計算する
-           self.last_move = None if step == 0 else self.records[step - 1]
+           self.last_move = self.records[step]
元と同じなので省略

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

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

```python
gui_play()
```

## 別の問題の検証と修正

リプレイ機能には、別の問題がまだ潜んでいます。ゲームをリセットし、下記の手順で操作を行ってください。

* (0, 0)、(1, 0)、(2, 0) の順で着手を行う
* << ボタンをクリックして最初の局面に戻す
* (1, 1) に着手を行う

上記の操作を行うと、下記の表示が行われます。一見すると問題はなさそうに思えるかもしれませんが、__下記の画像にはおかしな点があります__。それが何かを少し考えてみて下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/74392b46-432d-69d0-d0e7-ece365b59a99.png" width="200">

おかしな点は、__> や >>__ のボタンが __緑色で表示__ されて、__操作を行うことができる点__ です。__リプレイ中に他の着手を行った場合__ は、そこから __元のリプレイとは異なるゲームが行われる__ ことを意味します。そのため、リプレイ中に他の着手を行った場合は、__その局面が最後の着手を行った局面になる__ ので、__> や >>__ ボタンを __灰色で表示して操作できないようにする__ 必要があります。また、上記 で > をクリックすると、下図のように、__元の 2 手目の局面が表示される__ という問題が発生します。図は省略しますが、さらに > をクリックするか、>> ボタンをクリックすると、元の 3 手目の局面が表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/81470d8c-c086-a68e-bb04-5688d8fa5de8.png" width="200">

### 問題の検証

この問題は、__リプレイ中__ に、最後の着手が行われた以外の局面で着手を行った際に発生します。その場合は、下記の __`move`__ メソッドの 7 行目で __`self.board_records[self.move_count] = deepcopy(self.board)`__ が実行されますが、__その処理だけ__ では、__それ以降の手番のデータ__ が __そのまま残ってしまいます__。

```python
 1  def move(self, x, y):
 2      if self.place_mark(x, y, self.turn):
省略
 3          if len(self.board_records) <= self.move_count:            
 4              self.board_records.append(deepcopy(self.board))
 5              self.records.append(self.last_move)
 6          else:
 7              self.board_records[self.move_count] = deepcopy(self.board)
 8              self.records[self.move_count] = self.last_move
 9
10  Marubatsu.move = move
```

例えば、(0, 0)、(1, 0)、(2, 0) の順で着手を行い、<< ボタンをクリックすると、__それぞれの変数の値__ は以下のようになります。

| 変数 | 値 |
|:--|--:|
| `board` | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70"> |
| `board_records`<br>（変化しない）  | [ <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3eee9721-b553-46be-7794-d1d247b21063.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/87bada89-be66-1f7b-e356-228e318cc76e.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a13de375-fd10-8c11-2d17-8e72254fbbb5.png" width="70"> ] |
| `move_count`  | 0 |

この状態で、(1, 1) に着手を行うと、下記の表のように __`board_records[1]`__ に __`board`__ 属性を __コピーしたデータが代入__ されますが、__`board_records[2]`__ と __`board_records[3]`__ のデータは __元のまま残ってしまいます__。

| 変数 | 値 |
|:--|--:|
| `board` | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/661b811e-5690-673c-8a3c-94a356e4e223.png" width="70"> |
| `board_records`<br>（変化しない）  | [ <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/4769d8c7-e362-2e29-f036-197bed4436d2.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/661b811e-5690-673c-8a3c-94a356e4e223.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/87bada89-be66-1f7b-e356-228e318cc76e.png" width="70">, <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a13de375-fd10-8c11-2d17-8e72254fbbb5.png" width="70"> ] |
| `move_count`  | 0 |


### 問題の修正

この問題は、__`self.board_records[self.move_count] = deepcopy(self.board)`__ の処理を行った後で、__それ以降の要素を削除する__ ことで修正することができます。__list の特定の範囲の要素__ は、下記のプログラムの 9、10 行目のように、[以前の記事](https://qiita.com/ysgeso/items/6ca248133ff4d59a81d4#%E3%82%B9%E3%83%A9%E3%82%A4%E3%82%B9%E8%A1%A8%E8%A8%98-%E3%81%9D%E3%81%AE-1start-%E3%81%A8-end)で説明した __スライス表記__ を使って __取り出すことができます__。なお、先程と同様に、__`records`__ 属性に対しても __同様の処理を行う必要がある__ ことを忘れないようにして下さい。

:::note
スライス表記で 0 ~ x 番の要素を取り出したい場合は、`[0:x+1]` のように記述する必要がある点に注意して下さい。
:::

```python
 1  def move(self, x, y):
 2      if self.place_mark(x, y, self.turn):
元と同じなので省略
 3          if len(self.board_records) <= self.move_count:            
 4              self.board_records.append(deepcopy(self.board))
 5              self.records.append(self.last_move)
 6          else:
 7              self.board_records[self.move_count] = deepcopy(self.board)
 8              self.records[self.move_count] = self.last_move
 9              self.records = self.records[0:self.move_count + 1]
10              self.board_records = self.board_records[0:self.move_count + 1]  
11
12  Marubatsu.move = move
```

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

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

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

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

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

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


実行結果は省略しますが、上記の修正を行った後で、下記のプログラムを実行して先程の操作を行い、この問題が解消されたことを確認して下さい。

```python
gui_play()
```

# 今回の記事のまとめ

今回の記事では、__リプレイ機能の問題を修正__ しましたが、実は今回のプログラムには __AI が手番を担当した場合__ に __うまく動作しない__ という問題があります。次回の記事ではその問題を修正しますが、余裕がある方は、AI に手番を担当させた場合に、リプレイ機能に関するどのような問題が発生するかを調べておいてください。

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

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

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

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

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

# 次回の記事

https://qiita.com/ysgeso/items/05ac74365dad096dd28f