0
0

Pythonで〇×ゲームのAIを一から作成する その78 リプレイ機能の実装

Last updated at Posted at 2024-05-05

目次と前回の記事

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

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

ルールベースの AI の一覧

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

リプレイ機能

これまで のプログラムでは、play メソッドで AI どうし対戦GUI行った場合 に、AIすべての着手行った後 の、決着がついた ゲーム盤が 表示 されていました。今回の記事では、対戦途中経過表示 する、リプレイ機能実装 することにします。

リプレイ機能実装 するためには、リプレイを行うための、具体的な機能 について 検討 する必要があります。どのような機能必要か について少し考えてみて下さい。

リプレイに必要な機能

ぱっと思いつく リプレイ必要な機能 には、下記 のような機能があるでしょう。他にも必要な機能を思いついた方は、実装してみて下さい。

  • ゲーム開始時 まで 戻す 機能
  • 1 手戻す 機能
  • 1 手進める 機能
  • ゲームの終了時 まで 進める 機能

なお、実装を進めていけばすぐに気づくことができると思いますが、リプレイ機能 は、ゲームの決着ついていない場合 も行えた方が 便利 です。そこで、上記の 最後の機能 を「最後行われた着手 まで 進める 機能」に 変更 することにします。

次に、それぞれの機能 を呼び出すための UI決める必要あります。それについても少し考えてみて下さい。

リプレイ機能の UI の設定と実装

最も簡単UI としては、それぞれの機能 をリセットボタンなどと同様に、ボタンで呼び出す というものでしょう。本記事 でも ボタンの UI採用 することにします。

次に、ボタン表示 する 文字列 と、ボタンの配置考える必要あります。それらについても少し考えてみて下さい。

ボタンの表記と配置例

本記事では、それぞれの ボタン表示 する 文字列 を、動画プレーヤーなど で良く見かける 表記倣って下記 のような 文字列 とし、4 つのボタン横に並べて配置 することにします。また、4 つのボタン は、これまでに表示 していた、Dropdownリセットボタン下に配置 することにします。他の表記が良いと思った人は自由に変更してください。

機能 表記
ゲーム開始時まで戻す <<
1 手戻す <
1 手進める >
最後に行われた着手まで進める >>

リプレイ機能のウィジェットの作成と配置

上記の 4 つのボタンウィジェット は、リセットボタン同様の方法作成 することが できます。その際に、それぞれウィジェット代入 する 変数の名前下記 の表のように 名づける ことにします。なお、prev_buttonprev は、「前の」という 意味 を表す previous の略 です。

機能 表記 変数名
ゲーム開始時まで戻す << first_button
1 手戻す < prev_button
1 手進める > next_button
最後に行われた着手まで進める >> last_button

play メソッドの修正

上記の 4 つのボタンウィジェット は、Dropboxリセットボタン横に並べて配置 するために利用した、HBox を使って 横に配置 します。また、Dropbox などHBoxリプレイのボタンHBox は、以前の記事で説明したように、VBox を使って 縦に並べて配置 することが できます

下記 は、リプレイ機能ボタン作成 して 配置 するように play メソッドを 修正 したプログラムです。

  • ウィジェット配置 して 表示 する 処理一か所にまとめる ために、7 行目の下 にあった HBox作成 して 表示 する 処理43 行目 以降に 移動 する
  • 25 ~ 40 行目リプレイ機能4 つのボタン作成 し、変数に代入 する
  • 43 行目Dropdownリセットボタン横に並べて配置 する HBox作成 する
  • 45 行目リプレイ機能ボタン横に並べて配置 する HBox作成 する
  • 47 行目2 つHBox縦に並べて配置 する VBox作成 し、表示 する
 1  from marubatsu import Marubatsu
 2  import matplotlib.pyplot as plt
 3  import ipywidgets as widgets
 4  import math
 5
 6  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):              # 変更ボタンのイベントハンドラを定義する
元と同じなので省略
 7          # この下にあった hbox を作成するプログラムを 43 行目に移動する
 8
 9          def on_change_button_clicked(b):
10              for i in range(2):
11                  ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
12              self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
13
14          # リセットボタンのイベントハンドラを定義する
15          def on_reset_button_clicked(b):
16              self.restart()
17              on_change_button_clicked(b)
18
19          # イベントハンドラをボタンに結びつける
20          change_button.on_click(on_change_button_clicked)
21          reset_button.on_click(on_reset_button_clicked)        
22        
23          # 2 行目の UI を作成する
24          # リプレイのボタンを作成する
25          first_button = widgets.Button(
26              description="<<",
27              layout=widgets.Layout(width="100px"),
28          )
29          prev_button = widgets.Button(
30              description="<",
31              layout=widgets.Layout(width="100px"),
32          )
33          next_button = widgets.Button(
34              description=">",
35              layout=widgets.Layout(width="100px"),
36          )
37          last_button = widgets.Button(
38              description=">>",
39              layout=widgets.Layout(width="100px"),
40          )
41        
42          # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
43          hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
44          # リプレイ機能のボタンを横に配置した HBox を作成する
45          hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
46          # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
47          display(widgets.VBox([hbox1, hbox2]))   
元と同じなので省略  
48
49  Marubatsu.play = play        
行番号のないプログラム
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math

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

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

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

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

Marubatsu.play = play
修正箇所
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math

def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略       
-       # 〇 と × の dropdown とボタンを横に配置した HBox を作成し、表示する
-       hbox = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
-       display(hbox)
            
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)

        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)             
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
+       first_button = widgets.Button(
+           description="<<",
+           layout=widgets.Layout(width="100px"),
+       )
+       prev_button = widgets.Button(
+           description="<",
+           layout=widgets.Layout(width="100px"),
+       )
+       next_button = widgets.Button(
+           description=">",
+           layout=widgets.Layout(width="100px"),
+       )
+       last_button = widgets.Button(
+           description=">>",
+           layout=widgets.Layout(width="100px"),
+       )
        
        # 〇 と × の 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]))            
元と同じなので省略  

Marubatsu.play = play

上記修正後 に、下記 のプログラムで gui_play実行 すると、実行結果 のように、Dropdown の下リプレイ機能4 つのボタン並んで表示 されるようになります。

from util import gui_play

gui_play()

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

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

上記 のプログラムでは、ボタン作成するたび下記 のような 複数行 のプログラムを 記述する必要 があり、記述が面倒プログラム長くなる などの 問題あります

        first_button = widgets.Button(
            description="<<",
            layout=widgets.Layout(width="100px"),
        )

ボタンごと違い は、キーワード引数 description値だけ です。そこで、ボタン作成する処理 を行う 下記 のような 関数を定義 することにします。なお、ボタンの横幅設定できる便利 なので、横幅実引数設定できる ようにしました。

名前ボタンを作成する ので create_button とする
処理指定 した 表記横幅ボタンウィジェット作成 する
入力仮引数 descriptionボタン表示内容 を、widthボタン横幅代入 する
出力作成 した ウィジェット

この関数Marubatsu クラスの メソッド として 定義 する事は 可能 ですが、本記事 では 下記の理由 から play メソッドの ローカル関数 として 定義 する事にします。

  • 仮引数 self利用しない
  • この処理play メソッドの 中でしか利用しない

play メソッド 以外の場所利用 する 可能性がある 場合は、Marubatsu クラスの 静的メソッド として 定義しても良い でしょう。

他にも、通常の関数 として 定義 し、util.py保存 するという方法もあります。

下記は、create_button定義を追加 して 修正 した play メソッドです。

  • 3 ~ 7 行目create_buttonplay メソッドの ローカル関数 として 定義 する
  • 10、11、14 ~ 17 行目create_button利用 して ボタンを作成 するように 修正 する
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略       
 2          # ボタンを作成するローカル関数を定義する 
 3          def create_button(description, width):
 4              return widgets.Button(
 5                  description=description,
 6                  layout=widgets.Layout(width=f"{width}px"),
 7              )
 8       
 9          # 変更、リセットボタンを作成する
10          change_button = create_button("変更", 100)
11          reset_button = create_button("リセット", 100)
元と同じなので省略       
12          # 2 行目の UI を作成する
13          # リプレイのボタンを作成する
14          first_button = create_button("<<", 100)
15          prev_button = create_button("<", 100)
16          next_button = create_button(">", 100)
17          last_button = create_button(">>", 100)
元と同じなので省略       

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')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

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

Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略       
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
元と同じなので省略       
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
元と同じなので省略       

Marubatsu.play = play

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

gui_play()

リプレイ機能の実装

UI表示される ようになったので、次は それぞれのボタンクリックした際に実行 される イベントハンドラ定義 する 必要ありますイベントハンドラどのような処理記述 すれば良いかについて少し考えてみて下さい。

リプレイ機能が行う処理

リプレイ機能 では、4 種類ボタン を押すことで、ゲーム開始され てから 終了するまで任意の手数ゲーム盤表示 します。そのため、リプレイ機能実装 するためには、任意の手数指定 された際に、その手数ゲーム盤を表示 するという 処理行う必要あります。そこで、そのような処理 を行う 下記関数を定義 する事にします。

名前表示 する ゲーム手数(step)を 変更(change)するので change_step とする
処理指定 した 手数ゲーム盤画像を描画 する
入力仮引数 step手数代入 する
出力:なし

change_stepどのように実装 すればよいかについて少し考えてみて下さい。

ゲーム盤のデータを記録する方法

任意の手数ゲーム盤描画 するためには、その手数ゲーム盤再現 する 必要 があります。その方法一つ に、着手行うたび に、ゲーム盤 を表す borad 属性の データコピー して 記録しておく という方法があり、以下 のような 方法実装 できます。

  • 各手数ゲーム盤の情報記録 するための 属性の名前考える
  • ゲームリセット する 処理 を行う restart メソッドの で、その属性空の list代入 して 初期化 する
  • 着手 を行う move メソッドの で、その属性要素 に、着手後board 属性の 内容deepcopy(self.board)深いコピー を行ったものを 追加 する

ゲーム盤データ記録する際 は、代入や浅いコピーではなく、深いコピー行わなければならない 点に 注意 して下さい。

代入 では うまくいかない理由 は、list要素を追加 する際に行われる 代入オブジェクト共有 するという 処理 だからです。着手 を行うと board 属性に 代入 された list要素の値変化 しますが、board 属性に 代入 される オブジェクト変化しません。そのため、着手行うたびlistboard 属性を 追加 すると、すべての要素同一のオブジェクト代入される ことになります。

浅いコピー では うまくいかない理由 は、board 属性の 要素の値ミュータブル なデータである list だからです。忘れた方は、以前の記事復習 して下さい。

任意の手数ゲーム盤再現 する 他の方法 は、今後の記事説明します

record_boards 属性の実装

下記 は、属性の名前record_boards という 名前 にした場合の 実装例 です。それぞれメソッド最後の行追加 した プログラム です。プログラムを見ればわかると思いますが、行っている処理 は、着手を記録 する records 属性の 処理同様 です。

from copy import deepcopy

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.board_records = []

Marubatsu.restart= restart

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))
        
Marubatsu.move = move
修正箇所
from copy import deepcopy

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.board_records = []

Marubatsu.restart= restart

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))
        
Marubatsu.move = move

上記 のプログラムで、records 属性に 追加 する self.last_move深いコピーコピーするべき ではないかと 思った方いるかもしれない ので 補足 します。

self.last_move は、直前self.last_move = x, y で、新しい tuple代入 されるので、コピーを行わなくても records 属性の 他の要素値が共有される ことは ありません。それに対し、self.board着手 を行っても 同じオブジェクト参照し続ける ので、深いコピー行わずboard_records に追加 すると、board_records全ての要素同じオブジェクト代入される ことになります。

上記 のプログラムでは、着手 を記録する 属性 と、ゲーム盤 を記録する 属性別々の属性 にしましたが、下記 のプログラムのように、tupledict を使って、records 属性まとめて記録 することもできます。なお、下記 のプログラムは、move メソッド内で、記録を行う処理部分 です。

# tuple を利用する場合
self.records.append((self.last_move, deepcopy(self.board)))
# dict を利用する場合
self.records.append({
    "move": self.last_move,
    "board": deepcopy(self.board))
})

ただし、上記 のように records要素代入 する データの形式変更 すると、records 属性を 利用する これまでの プログラム動作しなくなる ので、本記事 では 上記の方法採用しない ことにします。

change_step の実装

下記は、change_step定義 です。

  • 3 行目step 手目の ゲーム盤データboard_records から 取り出し深いコピー を行ったデータを board に代入 する
  • 5 行目draw_board を使って、ゲーム盤の描画更新 する
1  def change_step(step):
2      # step 手目のゲーム盤のデータをコピーし、board に代入する
3      self.board = deepcopy(self.board_records[step])
4      # 描画を更新する
5      self.draw_board(ax, ai)

3 行目self.board = self.board_records[step] のように データコピーせず代入するデータ共有される ので バグが発生 します。具体的 には、着手 を行って self.board要素の値変化 すると self.board_records[step]要素の値同時変更 されてしまいます。

次に、この関数どこに記述する かを 考える必要ありますこの関数 では、self利用 しているので、Marubatsu クラスの メソッド として 定義 する方法が考えられますが、その場合は、axai仮引数 とする 必要がある 点が少し 面倒 です。一方、play メソッドの ローカル関数 として 定義 すれば、それらの名前そのまま利用 することが できる ので、本記事では play メソッドの ローカル関数 として 定義 します。

axai を、play メソッドの ローカル変数 として ではなくself.axself.ai のように、Marubatsu クラスの 属性にする ことで、change_stepMarubatsu クラスの メソッドとして定義 しても 仮引数 axai記述しなくても済む ようになります。ただし、今からそのようにプログラムを修正すると 大幅な修正必要になる ので 本記事 では 採用しない ことにします。

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

  • 9 ~ 13 行目change_step定義記述 する
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          # 2 行目の UI を作成する
 3          # リプレイのボタンを作成する
 4          first_button = create_button("<<", 100)
 5          prev_button = create_button("<", 100)
 6          next_button = create_button(">", 100)
 7          last_button = create_button(">>", 100)
 8       
 9          def change_step(step):
10              # step 手目のゲーム盤のデータをコピーし、board に代入する
11              self.board = self.board_records[step]
12              # 描画を更新する
13              self.draw_board(ax, ai)
元と同じなので省略        
14
15  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')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step 手目のゲーム盤のデータをコピーし、board に代入する
            self.board = deepcopy(self.board_records[step])
            # 描画を更新する
            self.draw_board(ax, ai)
        
        # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
        hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        # リプレイ機能のボタンを横に配置した HBox を作成する
        hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button]) 
        # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
        display(widgets.VBox([hbox1, hbox2]))        
  
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)

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

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

Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
+       def change_step(step):
+           # step 手目のゲーム盤のデータをコピーし、board に代入する
+           self.board = self.board_records[step]
+           # 描画を更新する
+           self.draw_board(ax, ai)
元と同じなので省略        

Marubatsu.play = play

ゲーム開始時まで戻すボタンのイベントハンドラの定義

ゲーム開始まで戻す ボタンの イベントハンドラ行う処理 は、1 手着手行っていない0 手目ゲーム盤の画像描画する処理 なので、change_turn(0) によって 行う ことが できます。そこで、その イベントハンドラ下記 のように on_first_button_clicked という 名前定義 し、ボタン結びつける ことにします。

  • 8、9 行目イベントハンドラ定義 する
  • 11 行目ボタンイベントハンドラ結び付ける
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          def change_step(step):
 3              # step 手目のゲーム盤のデータを board に代入する
 4              self.board = self.board_records[step]
 5              # 描画を更新する
 6              self.draw_board(ax, ai)
 7       
 8          def on_first_button_clicked(b):
 9              change_step(0)
10
11          first_button.on_click(on_first_button_clicked)
元と同じなので省略
12
13  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')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)        
        
        # 2 行目の UI を作成する
        # リプレイのボタンを作成する
        first_button = create_button("<<", 100)
        prev_button = create_button("<", 100)
        next_button = create_button(">", 100)
        last_button = create_button(">>", 100)
        
        def change_step(step):
            # step 手目のゲーム盤のデータを board に代入する
            self.board = self.board_records[step]
            # 描画を更新する
            self.draw_board(ax, ai)
        
        def on_first_button_clicked(b):
            change_step(0)

        first_button.on_click(on_first_button_clicked)

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

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

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

Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        def change_step(step):
            # step 手目のゲーム盤のデータを board に代入する
            self.board = self.board_records[step]
            # 描画を更新する
            self.draw_board(ax, ai)
        
+       def on_first_button_clicked(b):
+           change_step(0)

+       first_button.on_click(on_first_button_clicked)
元と同じなので省略

Marubatsu.play = play

上記修正後 に、下記 のプログラムで gui_play実行 し、いくつかの着手行った後 で、<< ボタンを クリック すると、実行結果 のように、1 手目着手行われた後ゲーム盤描画 されるという 問題が発生 します。

gui_play()

実行結果(下図は、1 手目に真ん中のマスに着手を行った場合の図です)

問題の原因の検証

on_first_button_clicked行う処理 は、change_step(0) だけ なので、問題の原因 は、change_step(0)行う処理調べる ことで 検証 できます。change_step(0) では、self.board = self.board_records[0]実行 するので、self.board_records[0]1 手目着手行った後ゲーム盤のデータ であることが 推測 されます。

board_record 属性に対して 行われる処理 は、以下の通りです。

  • ゲームリセット時 に、空の list初期化 する
  • 着手行うたび に、着手後ゲーム盤のデータ深いコピー要素追加 する

上記 から、ゲーム開始時board_records 属性の 空の list なので、1 手目ゲーム盤のデータ は、board_records0 番要素代入 されることが わかります

このことから、この問題原因ゲーム開始時ゲーム盤のデータboard_records 属性の 0 番要素代入されていない ことが 原因 であることが わかります

そのことは、ゲームの開始直後<< ボタンを クリック した際に、下記 のような エラーが発生 することからも 確認 できます。下記 のエラーは、空の要素代入 されている board_records0 番要素参照 しようとしたために 発生 します。

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[7], line 91
     90 def on_first_button_clicked(b):
---> 91     change_step(0)

Cell In[7], line 86
     84 def change_step(step):
     85     # step 手目のゲーム盤のデータを board に代入する
---> 86     self.board = self.board_records[step]
     87     # 描画を更新する
     88     self.draw_board(ax, ai)

IndexError: list index out of range

バグの修正

従って、下記 のプログラムの 9 行目 のように、restart メソッド内で、board_records0 番要素 に、ゲーム開始時ゲーム盤のデータコピーしたもの代入 するようにすることで この問題解決 できます。

 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)]
10
11  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.board_records = [deepcopy(self.board)]

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.board_records = []
+   self.board_records = [deepcopy(self.board)]

Marubatsu.restart = restart

上記修正後 に、下記 のプログラムで gui_play実行 し、いくつかの着手行った後 で、<< ボタンを クリック すると、実行結果 のように、ゲーム開始直後ゲーム盤描画 されます。また、ゲームの開始直後<< ボタンを クリック しても エラー発生しなくなる ことが 確認 できます。

gui_play()

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

<< ボタンの 処理 には いくつかの問題 があります。そのうちの 1 つ は、上図現れています。どのような問題があるかについて少し考えてみて下さい。

<< ボタンの処理の問題点 その 1

問題1 つ は、上図 のように、ゲーム開始時手番×なる場合がある という点です。ただし、<< ボタンを クリック した時に、手番〇 になる 場合も あります。そこで、<< ボタンを クリック した時に、どのような場合× の手番なるか色々と試して調べて みて下さい。

<< ボタンの処理の問題点 その 2

2 つ目の問題点は、ゲームの決着ついた場合 に、<< ボタンを クリック すると、下図 のように 決着ついた状態ゲーム開始時ゲーム盤表示される点 です。この問題の原因について少し考えてみて下さい。

<< ボタンの処理の問題点 その 3

着手 を行い << ボタンを クリック するという 作業繰り返す と、下図 のように すべてのマス埋まっていない のに 引き分け になるという 問題が発生 します。これが 3 つ目問題 です。この問題についても原因を少し考えてみて下さい。

なお、この後<< ボタンを クリック すると、引き分け状態ゲーム開始時ゲーム盤表示される という、2 つ目問題発生 します。正しい状態ゲーム開始 したい場合は、リセットボタンクリック して下さい。

1 つ目と 2 つ目の問題の原因の検証と修正

試行錯誤行う ことで、<< ボタンを クリック すると、以下の状態で ゲーム開始時ゲーム盤表示される ことがわかります。

  • ゲームの決着ついていない 場合は、<< ボタンを クリック した 時点の手番 になる
  • ゲームの決着ついている 場合は、決着ついたまま状態 になる

手番 の情報は turn 属性に、ゲームの決着がついているか どうかは status 属性に 代入 されていますが、change_step では、その いずれ属性の値変化しません。そのため、1 つ目2 つ目問題が発生 します。

そこで、下記 のプログラムのように、change_step の中で、turn 属性と status 属性の 値を計算 するという 処理記述 することで、1 つ目2 つ目問題解決 できます。

  • 6 行目〇×ゲームは偶数 手目が 〇 の手番 なので、手数 を表す step偶数の場合そうでない場合× を表す データturn 属性に 代入 する。偶数かどうか余り計算 する % 演算子 と、2割った余り0 であるか どうかで 判定 できる
  • 8 行目judge メソッドを使って status 属性の 計算し直す
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          def change_step(step):
 3              # step 手目のゲーム盤のデータをコピーし、board に代入する
 4              self.board = deepcopy(self.board_records[step])
 5              # 手番を計算する。step が偶数の場合は 〇 の 手番
 6              self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
 7              # status 属性を judget を使って計算する
 8              self.status = self.judge()
 9              # 描画を更新する
10              self.draw_board(ax, ai)
元と同じなので省略
11
12  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')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

        first_button.on_click(on_first_button_clicked)

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

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

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

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

Marubatsu.play = play

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

gui_play()

3 つ目の問題の原因の検証

さまざまな 試行錯誤繰り返す ことで わかるようになる と思いますが、この現象<< ボタンを クリック して ゲーム開始時局面戻した後発生 する現象です。リセット ボタンを クリック してゲームをリセットした 場合 には 発生しません

この問題は、引き分け関する現象 なので、引き分け判定方法調べる必要あります引き分け判定 は、judge メソッド内の 下記 のプログラムで行っています。

  elif self.is_full():
      return Marubatsu.DRAW

上記is_full下記 のように 定義 される メソッド です。

    def is_full(self):
        return self.move_count == self.BOARD_SIZE ** 2

上記 から、着手 した 回数 を表す move_count 属性が、ゲーム盤マスの数等しい場合引き分け判定 されることがわかります。そこで、move_count関する処理調べる と、move_count に対する 処理以下の場合行われる ことが わかります

  • restart メソッドで ゲームリセットした際 に、0 で初期化 される
  • move メソッドで 着手行われた際1 加算 される

従って、<< ボタンで ゲーム開始時局面表示 した場合は、move_count 属性の 変化しません。例えば、6 手目 まで 着手行った場合<< ボタンを クリック すると、ゲーム盤の表示ゲーム開始時表示戻ります が、着手行った回数 を表す move_count 属性の 6 のまま です。そのため、その後3 回着手 を行うと、move_count の値が 9 になり、下図 のように 引き分け になってしまいます。

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

3 つ目の問題の修正

この問題は、<< ボタンを クリック した時に、 move_count0代入する ことで 解決 できます。ただし、change_step は 0 手目だけでなく、任意の手数 に移動する処理を行うので、move_count には 0 ではなく、下記 のプログラムの 6 行目 のように、手数 を表す 仮引数 step をそのまま 代入 することで この問題解決 することが できます

 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          def change_step(step):
 3              # step 手目のゲーム盤のデータをコピーし、board に代入する
 4              self.board = deepcopy(self.board_records[step])
 5              # 手数を表す step を move_count に代入する
 6              self.move_count = step
 7              # 手番を計算する。step が偶数の場合は 〇 の 手番
 8              self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
 9              # status 属性を judget を使って計算する
10              self.status = self.judge()
11              # 描画を更新する
12              self.draw_board(ax, ai)       
元と同じなので省略
13
14  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')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

        first_button.on_click(on_first_button_clicked)

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

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

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

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

Marubatsu.play = play

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

gui_play()

残りボタンのイベントハンドラの定義

1 手戻す 処理と 1 手進む 処理は、現在表示 されている 局面の手数前後手数の局面表示 するという 処理 です。現在の手数move_count代入 されているので、その値change_step メソッドを使って イベントハンドラ記述 することが できます

最後行われた着手 まで 進める という 処理 は、慣れないとどのように記述すれば良いかわからないかもしれません。どこか最後行われた着手何手目 であるかを 表すデータ存在している ので、それが何か を少し考えてみて下さい。

board_records には ゲーム開始時 から、最後行われた着手 の局面までの ゲーム盤のデータlist の要素代入 されています。従って、list要素数計算 する 組み込み関数 len を使って、board_records要素の数 から、最後行われた着手手数計算 することが できますその際 に、__board_records__ には 着手行う前ゲーム開始時局面のデータ代入 されているため、len(board_records) - 1 のように、要素の数 から 1 を引く必要 がある点に 注意 して下さい。

下記は、上記イベントハンドラ と、ボタンイベントハンドラ結び付ける 処理を play メソッドに 記述 したプログラムです。

  • 5、6 行目1 手前手数self.move_count - 1計算 して 表示 する
  • 8、9 行目1 手後手数self.move_count + 1計算 して 表示 する
  • 11、12 行目最後行われた着手手数len(self.board_records) - 1計算 して 表示 する
  • 15 ~ 17 行目それぞれボタンイベントハンドラ結び付ける
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          def on_first_button_clicked(b):
 3              change_step(0)
 4
 5          def on_prev_button_clicked(b):
 6              change_step(self.move_count - 1)
 7
 8          def on_next_button_clicked(b):
 9              change_step(self.move_count + 1)
10            
11          def on_last_button_clicked(b):
12              change_step(len(self.board_records) - 1)
13
14          first_button.on_click(on_first_button_clicked)
15          prev_button.on_click(on_prev_button_clicked)
16          next_button.on_click(on_next_button_clicked)
17          last_button.on_click(on_last_button_clicked)
元と同じなので省略
18
19  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')
        
        # 1 行目の UI を作成する
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 〇 と × の Dropdown を格納する list
        dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # value を select_values に常に登録する
            select_values.append(value)
            # value が ai_values に登録済かどうかを判定する
            if value not in ai_dict.values():
                # 項目を登録する
                ai_dict[label] = value
        
            # Dropdown の description を計算する
            description = "" if i == 0 else "×"
            dropdown_list.append(
                widgets.Dropdown(
                    options=ai_dict,
                    description=description,
                    layout=widgets.Layout(width="100px"),
                    style={"description_width": "20px"},
                    value=select_values[i],
                )
            )
        
        # ボタンを作成するローカル関数を定義する 
        def create_button(description, width):
            return widgets.Button(
                description=description,
                layout=widgets.Layout(width=f"{width}px"),
            )
        
        # 変更、リセットボタンを作成する
        change_button = create_button("変更", 100)
        reset_button = create_button("リセット", 100)
              
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            for i in range(2):
                ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

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

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

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

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

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

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

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

Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
        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)
元と同じなので省略

Marubatsu.play = play

実行結果省略 しますが、上記修正後 に、下記 のプログラムで gui_play実行 し、4 つのボタンでリプレイを行うことができることを 確認 して下さい。

gui_play()

今回の記事のまとめ

今回の記事では、リプレイ機能 を行うための 4 つボタンの配置 と、イベントハンドラ定義 を行いました。それぞれボタン実際リプレイ を行うことが できます が、4 つのボタン に対して 様々な操作行う ことで、4 つのボタン行う処理 には いくつかの問題がある ことが わかる と思います。次回の記事 では それらの問題解決する方法紹介 するので、余裕がある方は どのような問題があるか について 調べておいてください

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

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

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

次回の記事

0
0
2

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
0