0
0

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

Last updated at Posted at 2024-05-09

目次と前回の記事

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

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

ルールベースの AI の一覧

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

強調について

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

リプレイ機能の問題点

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

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

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

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

from util import gui_play

gui_play()

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

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

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

  • ゲーム開始時の 0 手目の局面 では、直前に行われた着手存在しない ので、None を代入する必要がある
  • records 属性の 先頭の 0 番の要素 には、1 手目で行われた着手が記録 されるので、x 手目 の局面の直前の着手は、records[x - 1] に代入されている
インポートに関する記述は省略
 1
 2  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 3          def change_step(step):
 4              # step 手目のゲーム盤のデータをコピーし、board に代入する
 5              self.board = deepcopy(self.board_records[step])
 6              # 手数を表す step を move_count に代入する
 7              self.move_count = step
 8              # 手番を計算する。step が偶数の場合は 〇 の 手番
 9              self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
10              # status 属性を judget を使って計算する
11              self.status = self.judge()
12              # 直前の着手を計算する
13              self.last_move = None if step == 0 else self.records[step - 1]
14              # 描画を更新する
15              self.draw_board(ax, ai)
元と同じなので省略       
16
17  Marubatsu.play = play
行番号のないプログラム
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math
from copy import deepcopy

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

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

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

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

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

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

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

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

Marubatsu.play = play
修正箇所
インポートに関する記述は省略

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

Marubatsu.play = play

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

gui_play()

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

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

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

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

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

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

 

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

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

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

IndexError: list index out of range

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

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

問題の原因の検証

これらの問題は、以前の記事で説明した 負の整数のインデックス で行われる処理が原因です。上記では、ゲーム開始時の、0 手目の局面 で、1 手前の局面を表示 する < ボタンをクリックしました。0 手目の局面 では、手数を表す self.move_count には 0 が代入されている ので、< ボタンをクリックした際に実行される下記のイベントハンドラでは、-1 を実引数に記述 して change_step が呼び出されます。

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

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

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

1  def change_step(step):
2      # step 手目のゲーム盤のデータをコピーし、board に代入する
3      self.board = deepcopy(self.board_records[step])
4      # 手数を表す step を move_count に代入する
5      self.move_count = step     

6      # 直前の着手を計算する
7      self.last_move = None if step == 0 else self.records[step - 1]
8      # 描画を更新する
9      self.draw_board(ax, ai)

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

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

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

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

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

バグの修正方法その 1

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

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

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

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

step = 0 if step < 0 else step

また、実引数の中の最大値を返す処理を行う、以前の記事で紹介した、組み込み関数 max を使って下記のように、より簡潔に記述できます。

step = max(0, step)

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

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

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

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

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

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

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

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

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

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

Marubatsu.play = play

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

gui_play()

バグの修正方法 その 2

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

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

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

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

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

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

prev_button.disabled = (self.move_count == 0)

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

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

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

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

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

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

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

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

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

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

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

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

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

Marubatsu.play = play

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

gui_play()

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

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

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

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

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

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

この問題を解決する方法としては、ボタンのウィジェットの 表示のスタイル を表す style 属性を使って、ボタンの表示の色を変更する という方法があります。

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

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

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

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

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

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

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

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

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

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

Marubatsu.play = play

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

gui_play()

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

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

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

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

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

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

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

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

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

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

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

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

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

Marubatsu.play = play

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

gui_play()

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

update_widgets_status の定義

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

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

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

下記は update_widgets_status の定義です。

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

この関数内では、selfprev_button に関する処理を行う必要があるので、前回の記事のボタンを作成する関数と同様の理由で update_widgets_statusplay メソッドのローカル関数として定義 します。また、play メソッド内で restart メソッドを実行した直後と、on_mouse_down 内で人間が着手を行った際にこの関数を呼び出すように修正します。

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

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

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

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

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

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

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

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

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

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

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

        # ウィジェットの状態を更新する        
+       def update_widgets_status():
            # move_count が 0 以下の場合に < ボタンを操作できないようにし、色を変更する
+           prev_button.disabled = self.move_count <= 0
+           prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
元と同じなので省略        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax, ai)
+               update_widgets_status()
元と同じなので省略
    self.restart()
-   prev_button.disabled = self.move_count <= 0
-   prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"
+   update_widgets_status()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play

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

gui_play()

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

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

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

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

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

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

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

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

  • 5、13 行目:リセットボタンがクリックされた時と、リプレイ機能のボタンがクリックされた時に update_widgets_status を呼び出す
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          # リセットボタンのイベントハンドラを定義する
 3          def on_reset_button_clicked(b):
 4              self.restart()
 5              update_widgets_status()
 6              on_change_button_clicked(b)
元と同じなので省略
 7          def change_step(step):
 8              # step が負の場合は 0 に修正する
 9              step = max(0, step)
元と同じなので省略
10              # 描画を更新する
11              self.draw_board(ax, ai)
12              # ウィジェットの状態を更新する
13              update_widgets_status()
元と同じなので省略
14
15  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')

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

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

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

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

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

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

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

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

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

Marubatsu.play = play

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

gui_play()

2 つの修正方法の併用

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

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

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

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

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

<< ボタンに関する修正

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

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

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

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

  • 7、8 行目:<< ボタンも < ボタンと同様に 0 手目以下の場合に操作できないようにする
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          # ウィジェットの状態を更新する        
 3          def update_widgets_status():
 4              # move_count が 0 以下の場合に < ボタンと << ボタンを操作できないようにし、色を変更する
 5              prev_button.disabled = self.move_count <= 0
 6              prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
 7              first_button.disabled = self.move_count <= 0
 8              first_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
元と同じなので省略        
 9
10  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')

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

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

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

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

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

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

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

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

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

Marubatsu.play = play

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

gui_play()

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

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

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

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

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

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

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

  • 8 ~ 10 行目:ボタンのウィジェットの状態を設定する set_button_status を定義する
  • 15、16 行目set_button_status を利用して、<< と < ボタンの状態を更新する
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2      # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
 3      if gui:
 4          # %matplotlib widget のマジックコマンドを実行する
 5          get_ipython().run_line_magic('matplotlib', 'widget')
 6
 7          # ボタンのウィジェットの状態を設定する
 8          def set_button_status(button, disabled):
 9              button.disabled = disabled
10              button.style.button_color = "lightgray" if disabled else "lightgreen"
11
12          # ウィジェットの状態を更新する        
13          def update_widgets_status():
14              # 0 手目以下の場合に、<< と < ボタンを操作できないようにする
15              set_button_status(first_button, self.move_count <= 0)
16              set_button_status(prev_button, self.move_count <= 0)
元と同じなので省略
17
18  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
  
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
        get_ipython().run_line_magic('matplotlib', 'widget')

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

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

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

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

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

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

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

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

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

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

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

        # ウィジェットの状態を更新する        
        def update_widgets_status():
            # 0 手目以下の場合に、<< と < ボタンを操作できないようにする
-           prev_button.disabled = self.move_count <= 0
-           prev_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"        
-           first_button.disabled = self.move_count <= 0
-           first_button.style.button_color = "lightgray" if self.move_count <= 0 else "lightgreen"  
+           set_button_status(first_button, self.move_count <= 0)
+           set_button_status(prev_button, self.move_count <= 0)
元と同じなので省略

Marubatsu.play = play

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

gui_play()

今回の記事のまとめ

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

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

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

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

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

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

次回の記事

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

0
0
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
0