0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-05-12

目次と前回の記事

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

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

ルールベースの AI の一覧

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

リプレイ機能の問題点(再掲)

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

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

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

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

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] で、stepboard_records に代入された list の範囲外のインデックス であることです。実際に、最後の着手が行われた局面で > ボタンをクリックすると step には 最後の着手の局面の次の要素のインデックス が代入されますが、board_records には そのような要素は存在しません

このエラーを修正するためには、前回の記事で < ボタンに対して同じ list index out of range のエラーが発生した際に行った修正と同様に、下記のプログラムの 4 行目のように、stepself.board_records の要素の数以上になった場合 に、stepself.board_records最後のインデックスの値を代入 するように修正する必要があります。

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 行でまとめることもできますが、わかりづらいと思った方は上記のプログラムを採用して下さい。

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

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

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
プログラム全体
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
修正箇所
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

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

gui_play()

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

<< ボタンや < ボタンと同様に、> と >> ボタン も、最後の着手が行われた局面操作をできないように修正する ことにします。それらのボタンが 操作を行えない条件 は、move_count 属性が self.board_records に代入された list の要素の数 - 1 以上 の場合なので、set_button_status を使って下記のプログラムのように play メソッドを修正します。

  • 7、8 行目:> と >> ボタンに対して、move_count 属性が最後の着手が行われた手数以上の場合に、操作できないようにする
 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
行番号のないプログラム
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
修正箇所
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

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

gui_play()

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

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

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

操作 表示
1 (0, 0)、(1, 0)、(2, 0) の順で着手を行う
2 < ボタンをクリックして 2 手目の局面に戻す
3 (0, 1) のマスをクリックして着手を行う
4 > ボタンをクリックして次の手数の局面を表示する
5 < ボタンをクリックして前の手数の局面を表示する

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

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

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

問題の検証

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

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

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

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 行目の処理によって、それぞれの変数には下記の表のデータが代入されます。なお、わかりやすさを重視して、boardboard_records要素のデータ には、ゲーム盤の画像を表記 します。

変数
board
board_records [ ]
move_count 0

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

操作 1 の検証

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

 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
(3 手目の局面になる)
board_records
(0 ~ 3 手目の局面が代入される)
[ , , , ]
move_count 3

操作 2 の検証

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

 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 行目で 仮引数 stepself.move_count - 1、すなわち 3 - 1 = 2 が代入される
  • 6 行目で board 属性に self.board_records[2] をコピーしたものが代入される
  • 7 行目で move_count 属性に 2 が代入される

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

変数
board
(2 手目の局面になる)
board_records
(変化しない)
[ , , , ]
move_count 2

操作 3 の検証

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

 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
board_records [ , , , , ]
move_count 3

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

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

[ , , , ]

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

 

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

操作 4 の検証

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

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

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

変数
board
board_records
(変化しない)
[ , , , , ]
move_count 4

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

操作 5 の検証

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

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

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

変数
board
board_records
(変化しない)
[ , , , , ]
move_count 3

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

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

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

問題の修正

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

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

 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
行番号のないプログラム
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
修正箇所
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

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

問題の検証と修正

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

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 を使って要素を追加 し、そうでなければ 上記のように 代入を行う ようにすることで修正することができます。

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
行番号のないプログラム
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
修正箇所
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

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

gui_play()

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

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

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

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

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

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 と同じ方法で、最後に行われた着手のデータを記録する
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
行番号のないプログラム
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
修正箇所
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

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

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 つだけ小さくなります

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

また、print を書いたり消したりするのが面倒な場合は、test_judgeで実装したような、デバッグ表示の機能を実装するという方法もあります。

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

 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
行番号のないプログラム
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
修正箇所
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

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

gui_play()

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

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

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 で初期化するように修正する
1  def restart(self):
元と同じなので省略
2      self.records = [self.last_turn]
3      self.board_records = [deepcopy(self.board)]  
4
5  Marubatsu.restart = restart        
行番号のないプログラム
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        
修正箇所
def restart(self):
元と同じなので省略
-   self.records = []
+   self.records = [self.last_turn]
    self.board_records = [deepcopy(self.board)]  

Marubatsu.restart = restart        

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

 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
行番号のないプログラム
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
修正箇所
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

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

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

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
行番号のないプログラム
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
修正箇所
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

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

gui_play()

別の問題の検証と修正

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

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

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

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

問題の検証

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

 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
board_records
(変化しない)
[ , , , ]
move_count 0

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

変数
board
board_records
(変化しない)
[ , , , ]
move_count 0

問題の修正

この問題は、self.board_records[self.move_count] = deepcopy(self.board) の処理を行った後で、それ以降の要素を削除する ことで修正することができます。list の特定の範囲の要素 は、下記のプログラムの 9、10 行目のように、以前の記事で説明した スライス表記 を使って 取り出すことができます。なお、先程と同様に、records 属性に対しても 同様の処理を行う必要がある ことを忘れないようにして下さい。

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

 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
行番号のないプログラム
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
修正箇所
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

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

gui_play()

今回の記事のまとめ

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

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

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

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

次回の記事

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?