0
0

Pythonで〇×ゲームのAIを一から作成する その77 Dropdown による AI の選択と play メソッドの改良

Last updated at Posted at 2024-05-02

目次と前回の記事

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

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

ルールベースの AI の一覧

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

play メソッドのバグの修正

今回の記事を記述する際に気が付いたのですが、play メソッドには バグ があります。それは、下記 のプログラムのように、キーワード引数 gui記述せずplay メソッドを呼び出して CUI〇×ゲーム遊ぼうとした場合 に、実行結果 のように Dropdown が表示 され、さらに エラーが発生 する点です。これらの原因について少し考えてみて下さい。

from marubatsu import Marubatsu
from ai import ai1s

mb = Marubatsu()
mb.play(ai=[ai1s, ai1s])

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

---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Cell In[1], line 5
      2 from ai import ai1s
      4 mb = Marubatsu()
----> 5 mb.play(ai=[ai1s, ai1s])

File c:\Users\ys\ai\marubatsu\077\marubatsu.py:382, in Marubatsu.play(self, ai, ai_dict, params, verbose, seed, gui, size)
    379     fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    381 self.restart()
--> 382 return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

UnboundLocalError: cannot access local variable 'ax' where it is not associated with a value

上記のエラーメッセージは、以下のような意味を持ちます。

  • UnboundLocalError
    束縛されていない(unbound)ローカル(local)な名前に関するエラー
  • cannot access local variable 'ax' where it is not associated with a value
    値(value)が関連付けられていない(associated with)ax というローカル変数(local variable)にアクセス(access)することができない(cannot)

エラーの原因の検証

変数値を代入 することを、束縛 する(bound)、または 関連付ける などと呼びます。従って、この エラーメッセージ は、ax という ローカル変数代入されていない という 意味表します。そこで、play メソッドの で、ax値を代入 する 処理探してみる と、下記 の部分で 行われている ことがわかります。

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])

上記 のプログラムの if gui: から、ax は、guiTrue が代入 されている 場合のみ値が代入される ことが わかります先程 のように、キーワード引数 gui記述せずplay メソッドを 呼び出す と、gui には デフォルト値 である False代入 されるので、エラーメッセージが示すように、ax代入されていない ことが 確認 できました。

エラーの修正方法

このエラーは、play_loop呼び出す際 に、ax代入されていない ことが 原因 なので、guiFalse の場合 に、ax何らかの値代入 することで 修正 できます。そこで、axどのような値代入すればよいか について 検討 するために、play_loop で、axどのように利用 されているかについて 調べる ことにします。

play_loopax関する処理行われる のは、下記4、6 行目 の部分です。


1  if gui:
2      # AI どうしの対戦の場合は画面を描画しない
3      if ai[0] is None or ai[1] is None:
4          self.draw_board(ax)

5  if gui:
6      self.draw_board(ax)

いずれ も、guiTrue代入 されている 場合のみ実行される処理 なので、guiFalse代入 されている場合は、play_loop では ax利用されない ことが わかります。そのため、guiFalse代入 されている場合は、ax何を代入 しても かまわない ことが わかりました。そこで、本記事 では 下記 のプログラムのように、guiFalse が代入 されていた場合は、axNone を代入 することにします。

  • 8、9 行目guiTrue でない、すなわち False の場合axNone代入するこの処理10 行目より前実行 することで、play_loop呼び出す際 に、ax必ず代入される ようになる
 1  import matplotlib.pyplot as plt
 2  import ipywidgets as widgets
 3  import math
 4
 5  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 6      # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
 7      if gui:
元と同じなので省略
 8      else:
 9          ax = None
元と同じなので省略
10      return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
11
12  Marubatsu.play = play 
行番号のないプログラム
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)
    
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 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 を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_dict,
        description="",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_dict,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[1],
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui) 
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                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 
修正箇所
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):      
元と同じなので省略
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
元と同じなので省略
+   else:
+       ax = None
元と同じなので省略
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play 

上記修正後 に、下記 のプログラムを 実行 すると、エラー発生しなくなります が、実行結果 のように CUI で〇×ゲームを遊んでいるにも 関わらず 、最初に Dropdown が表示 されてしまうという 問題解消されていない ことが わかります

mb.play(ai=[ai1s, ai1s])

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

Turn o
...
...
...
以下略(ai1s どうしの対戦が表示される)

CUI での Dropdown の表示の修正

guiFalse が代入 されている場合でも Dropdown が表示 されるのは、play メソッド内で、Dropdown の処理gui代入 されている 関わらず必ず実行 されるように 記述されている からです。下記 のプログラムのように、それらの処理if gui:ブロックの中移動 することで、この問題解決 することが できます。なお、下記 のプログラムは、guiブロックの中移動 した プログラムのみ を示します。

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 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 を作成する   
        dropdown_circle = widgets.Dropdown(
            options=ai_dict,
            description="",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
            value=select_values[0],
        )
        dropdown_cross = widgets.Dropdown(
            options=ai_dict,
            description="×",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
            value=select_values[1],
        )       
        
        # リセットボタンを作成する
        button = widgets.Button(
            description="リセット",
            layout=widgets.Layout(width="100px"),
        )
        
        # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
        hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
        display(hbox)
        
        # リセットボタンのイベントハンドラを定義する
        def on_button_clicked(b):
            self.restart()
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
プログラム全体
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:
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 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 を作成する   
        dropdown_circle = widgets.Dropdown(
            options=ai_dict,
            description="",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
            value=select_values[0],
        )
        dropdown_cross = widgets.Dropdown(
            options=ai_dict,
            description="×",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
            value=select_values[1],
        )       
        
        # リセットボタンを作成する
        button = widgets.Button(
            description="リセット",
            layout=widgets.Layout(width="100px"),
        )
        
        # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
        hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
        display(hbox)
        
        # リセットボタンのイベントハンドラを定義する
        def on_button_clicked(b):
            self.restart()
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui) 
            
        # イベントハンドラをリセットボタンに結びつける
        button.on_click(on_button_clicked)

        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)

                # 次の手番の処理を行うメソッドを呼び出す
                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

上記修正後 に、下記 のプログラムを 実行 すると、実行結果 のように Dropdown表示されなく なり、CUI〇×ゲーム実行される ようになります。

mb.play(ai=[ai1s, ai1s])

実行結果

Turn o
...
...
...
以下略(ai1s どうしの対戦が表示される)

バグを見逃した原因

今回のバグ を これまで 見逃し ていた 原因 は、GUI〇×ゲーム遊べるようplay メソッドを 修正した際 に、CUI での play メソッドの 処理正しく動作するか確認しなかった ためです。このように、プログラムで 何かを修正 した場合は、それまで行えていた処理正しく動作するか確認 することを 怠らない ことが 重要 です。

マジックコマンドを Python で実行する方法

これまで のプログラムでは、play メソッドの 実引数gui=True記述 して 〇×ゲームGUI遊ぶ際 に、%matplotlib widget という マジックコマンド実行する必要ありました以前の記事で、マジックコマンド は、Pythonプログラムではない ので、marubatsu.py記述 すると エラーが発生 するため、JupyterLab 上実行 する 必要がある と説明しましたが、下記 のプログラムで、%matplotlib widgetマジックコマンドPythonプログラム実行できます

get_ipython().run_line_magic('matplotlib', 'widget')

%matplotlib widget 以外マジックコマンド同様の方法実行できます

get_ipython は、IPython モジュールで 定義 されていますが、インポートしなくても利用できる ようです。get_ipython実行 した際に エラーが発生 した場合は、下記 のプログラムを 実行 して インポート して下さい。

from IPython import get_ipython

そこで、下記 のプログラムの 5 行目 のように、guiTrue代入 されている 場合上記処理を行う ように play メソッドを 修正 することにします。

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  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')
        
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 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 を作成する   
        dropdown_circle = widgets.Dropdown(
            options=ai_dict,
            description="",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
            value=select_values[0],
        )
        dropdown_cross = widgets.Dropdown(
            options=ai_dict,
            description="×",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
            value=select_values[1],
        )       
        
        # リセットボタンを作成する
        button = widgets.Button(
            description="リセット",
            layout=widgets.Layout(width="100px"),
        )
        
        # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
        hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
        display(hbox)
        
        # リセットボタンのイベントハンドラを定義する
        def on_button_clicked(b):
            self.restart()
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
            
        # イベントハンドラをリセットボタンに結びつける
        button.on_click(on_button_clicked)
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                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):      
元と同じなので省略
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # %matplotlib widget のマジックコマンドを実行する
+       get_ipython().run_line_magic('matplotlib', 'widget')
元と同じなので省略

上記修正後 に、下記 のプログラムで gui_play実行 すると、実行結果 のように、%matplotlib widget実行しなくても GUI で 〇×ゲームを遊べる ことが 確認できます

from util import gui_play

gui_play()

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

Dropdown による AI の選択

これまで作成 した play メソッドは、手番を担当 する AI を選択する ための Dropdown表示行うだけ で、Dropdown選択を変更 しても 何の処理行われません。そこで、Dropdown選択を変更 することで、手番を担当 する AI変更する ようにします。

なお、実際 には、Dropdown人間を選択 することも できます が、毎回「DropdownAI または人間選択 する」のように 表記 するのは 冗長 なので、以後 は、人間の選択省略 して、「DropdownAI を選択 する」のように 表記 します。

Dropdown の選択の変更を反映させるタイミング

まず、Dropdown選択の変更どの時点〇×ゲーム反映させるか決める必要あります候補 としては 下記3 つ が挙げられるでしょう。

  1. Dropdown変更された時点即座変更を反映 する
  2. ゲームリセットした時点変更を反映 し、新しい対戦を開始する
  3. Dropdown変更を適用 する ボタンを用意 し、押された時変更を反映 する

上記どれを採用 しても 構いません が、1 の「Dropdown変更された時点即座変更を反映 する」という 方法 は、Dropdown操作ミス によって 意図しない変更 が行われてしまう 可能性がある ので、本記事 では 23方法採用 することにします。

ゲームのリセットによる Dropdown の反映

以前の記事で説明したように、Dropdown選択中項目の値 は、Dropdownvalue 属性代入 されています。そのため、下記 のプログラムのように、リセットボタンイベントハンドラ である on_button_clicked修正 することで、ボタンクリックした時 に、Dropdown選択 されている AI新しい対戦開始される ようになります。

  • 5 行目〇 の担当 を表す Dropdownvalue 属性が "人間" の場合は、ai[0]人間の担当 を表す None を代入 する。それ以外 の場合は、value 属性に AI関数オブジェクト代入されている ので、value 属性の ai[0]そのまま代入 する
  • 6 行目上記同様の処理× の担当 を表す Dropdown に対して 行う
1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略      
2          # リセットボタンのイベントハンドラを定義する
3          def on_button_clicked(b):
4              self.restart()
5              ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
6              ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
7              self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略
8
9  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')
        
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 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 を作成する   
        dropdown_circle = widgets.Dropdown(
            options=ai_dict,
            description="",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
            value=select_values[0],
        )
        dropdown_cross = widgets.Dropdown(
            options=ai_dict,
            description="×",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
            value=select_values[1],
        )       
        
        # リセットボタンを作成する
        button = widgets.Button(
            description="リセット",
            layout=widgets.Layout(width="100px"),
        )
        
        # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
        hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
        display(hbox)
        
        # リセットボタンのイベントハンドラを定義する
        def on_button_clicked(b):
            self.restart()
            ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
            ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
            
        # イベントハンドラをリセットボタンに結びつける
        button.on_click(on_button_clicked)
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                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_button_clicked(b):
            self.restart()
+           ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
+           ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略

Marubatsu.play = play

上記修正後 に、下記 のプログラムで gui_play実行した後 で、Dropdown変更 してから リセットボタン押す と、変更した AI新しい対戦開始 されます。実行結果省略 しますが、実際Dropdown様々な組み合わせ選択 して 確認 して下さい。

gui_play()

変更ボタンによる Dropdown の反映

AI の担当変更するボタン行う処理 は、ゲームリセットする処理除けばリセットボタン行う処理同じ です。そのため、その処理下記 のプログラムのように 記述できます

なお、ボタン2 つ増えた ので、それぞれボタンウィジェット代入する変数名前 を、change_buttonreset_button のようにして 区別できる ように しましたボタンクリック した場合の イベントハンドラ名前同様 です。

  • 3 ~ 6 行目変更ボタンウィジェット作成 する
  • 9 行目リセットボタンウィジェット代入 する 変数の名前reset_button変更 する
  • 15 行目HBox変更ボタンウィジェット追加 する
  • 19 ~ 22 行目変更ボタンクリック した際の イベントハンドラ定義 する
  • 25 行目リセットボタンクリック した際の イベントハンドラ名前on_reset_button_clicked変更 する
  • 32、33 行目:それぞれの ボタン をそれぞれの イベントハンドラ結びつける
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          # 変更ボタンを作成する
 3          change_button = widgets.Button(
 4              description="変更",
 5              layout=widgets.Layout(width="100px"),
 6          )
 7       
 8          # リセットボタンを作成する
 9          reset_button = widgets.Button(
10              description="リセット",
11              layout=widgets.Layout(width="100px"),
12          )
13        
14          # 〇 と × の dropdown と ボタンを横に配置した HBox を作成し、表示する
15        hbox = widgets.HBox([dropdown_circle, dropdown_cross, change_button, reset_button])
16          display(hbox)
17       
18          # 変更ボタンのイベントハンドラを定義する
19          def on_change_button_clicked(b):
20              ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
21              ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
22              self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
23
24          # リセットボタンのイベントハンドラを定義する
25          def on_reset_button_clicked(b):
26              self.restart()
27              ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
28              ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
29              self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
30            
31          # イベントハンドラをボタンに結びつける
32          change_button.on_click(on_change_button_clicked)
33          reset_button.on_click(on_reset_button_clicked)
元と同じなので省略
34
35  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')
        
        # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
        select_values = []
        # 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 を作成する   
        dropdown_circle = widgets.Dropdown(
            options=ai_dict,
            description="",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
            value=select_values[0],
        )
        dropdown_cross = widgets.Dropdown(
            options=ai_dict,
            description="×",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
            value=select_values[1],
        )       
        
        # 変更ボタンを作成する
        change_button = widgets.Button(
            description="変更",
            layout=widgets.Layout(width="100px"),
        )
        
        # リセットボタンを作成する
        reset_button = widgets.Button(
            description="リセット",
            layout=widgets.Layout(width="100px"),
        )
        
        # 〇 と × の dropdown と ボタンを横に配置した HBox を作成し、表示する
        hbox = widgets.HBox([dropdown_circle, dropdown_cross, change_button, reset_button])
        display(hbox)
        
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
            ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
            ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
        def on_reset_button_clicked(b):
            self.restart()
            ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
            ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                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):      
元と同じなので省略
        # 変更ボタンを作成する
+       change_button = widgets.Button(
+           description="変更",
+           layout=widgets.Layout(width="100px"),
+       )
        
        # リセットボタンを作成する
-       button = widgets.Button(
+       reset_button = widgets.Button(
            description="リセット",
            layout=widgets.Layout(width="100px"),
        )
        
        # 〇 と × の dropdown と ボタンを横に配置した HBox を作成し、表示する
-       hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
+       hbox = widgets.HBox([dropdown_circle, dropdown_cross, change_button, reset_button])
        display(hbox)
        
        # 変更ボタンのイベントハンドラを定義する
+       def on_change_button_clicked(b):
+           ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
+           ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
+           self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

        # リセットボタンのイベントハンドラを定義する
-       def on_button_clicked(b):
+       def on_reset_button_clicked(b):
            self.restart()
            ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
            ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
            
        # イベントハンドラをボタンに結びつける
+       change_button.on_click(on_change_button_clicked)
-       button.on_click(on_button_clicked)
+       reset_button.on_click(on_reset_button_clicked)
元と同じなので省略

Marubatsu.play = play

上記修正後 に、下記 のプログラムで gui_play実行 すると、実行結果 のように、変更ボタン表示される ようになります。また、Dropdown変更 してから 変更ボタン押す と、ゲームリセットされず に、AI担当のみ変更 されます。実行結果省略 しますが、実際Dropdown様々な組み合わせ選択 して 確認 して下さい。

gui_play()

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

変更ボタンのイベントハンドラに関する注意点

変更ボタンイベントハンドラ処理 は、それぞれの 手番を担当 する AI を変更 する 処理 なので、下記 のプログラムの 5 行目self_loop呼び出す処理必要ない のではないかと 思った人いないでしょうか?しかし、下記の 5 行目削除 すると バグが発生 します。どのようなバグが発生するかについて少し考えてみて下さい。

1  # 変更ボタンのイベントハンドラを定義する
2  def on_change_button_clicked(b):
3      ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
4      ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
5      self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

確かに、上記の 5 行目削除しても それぞれの 手番担当する AI変化 しますが、人間の手番 の時に、人間から AI に変更 しても、変更した AI着手行わない という 問題が発生 します。その 理由 は上記の 5 行目 が、現在の手番AI の場合着手選択する処理行っている からです。なお、この問題の 原因 は、以前の記事で説明した、リセットボタンクリック後AI着手を行わない 問題と 同じ です。

play メソッドの改良

これで、play メソッドの 実行後 に、手番担当する AIDropdown変更できる ようになりましたが、play メソッドにはいくつか 改良 できる 余地がある ので、それらの 改良を行う ことにします。改良案についていくつか考えてみて下さい。

Dropdown に関する処理の改良

これまでplay メソッドでは、Dropdown に関する 処理 を、2 つDropdown ごと記述 してきましたが、この 2 つDropdown関する処理ほとんど同じ処理 なので、for 文 を使って 下記 のプログラムのように まとめる ことが できます

  • 3 行目2 つDropdown代入する変数 drop_list空の list初期化 する
  • 7 ~ 16 行目2 つDropdown作成する処理 を、for 文ブロック内記述 する
  • 7 行目Dropdown左に表示 する 文字列 を、i の値応じて計算 する
  • 8 ~ 16 行目Dropdown作成 し、dropdown_list要素追加 する
  • 11 行目:キーワード引数 description7 行目計算 した 文字列を指定 する
  • 14 行目:キーワード引数 valueselect_values[i]指定 する
  • 18 行目HBoxdropdown_list各要素記述 するように 修正 する
  • 23、24、30、31 行目ai各要素Dropdown選択項目の値代入 する 処理 を、繰り返し処理 を使って 行う ように 修正 する
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          # 〇 と × の Dropdown を格納する list
 3          dropdown_list = []
 4          # ai に代入されている内容を ai_dict に追加する
 5          for i in range(2):
元と同じなので省略      
 6              # Dropdown の description を計算する
 7              description = "" if i == 0 else "×"
 8              dropdown_list.append(
 9                  widgets.Dropdown(
10                      options=ai_dict,
11                      description=description,
12                      layout=widgets.Layout(width="100px"),
13                      style={"description_width": "20px"},
14                      value=select_values[i],
15                  )
16              )
元と同じなので省略      
17          # 〇 と × の dropdown とボタンを横に配置した HBox を作成し、表示する
18          hbox = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
19          display(hbox)
20        
21          # 変更ボタンのイベントハンドラを定義する
22          def on_change_button_clicked(b):
23              for i in range(2):
24                  ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
25              self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui) 
26
27          # リセットボタンのイベントハンドラを定義する
28          def on_reset_button_clicked(b):
29              self.restart()
30              for i in range(2):
31                  ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
32              self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)    元と同じなので省略
33
34  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')
        
        # それぞれの手番の担当を表す 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"),
        )
        
        # 〇 と × の 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()
            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)
            
        # イベントハンドラをボタンに結びつける
        change_button.on_click(on_change_button_clicked)
        reset_button.on_click(on_reset_button_clicked)
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                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):      
元と同じなので省略
        # 〇 と × の Dropdown を格納する list
+       dropdown_list = []
        # ai に代入されている内容を ai_dict に追加する
        for i in range(2):
元と同じなので省略      
            # 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],
+               )
+           )

        # 〇 と × の Dropdown を作成する   
-       dropdown_circle = widgets.Dropdown(
-            options=ai_dict,
-            description="",
-            layout=widgets.Layout(width="100px"),
-            style={"description_width": "20px"},
-            value=select_values[0],
-        )
-        dropdown_cross = widgets.Dropdown(
-            options=ai_dict,
-            description="×",
-            layout=widgets.Layout(width="100px"),
-            style={"description_width": "20px"},
-            value=select_values[1],
-        )               
元と同じなので省略      
        # 〇 と × の dropdown とボタンを横に配置した HBox を作成し、表示する
-       hbox = widgets.HBox([dropdown_circle, dropdown_cross, change_button, reset_button])
+       hbox = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
        display(hbox)
        
        # 変更ボタンのイベントハンドラを定義する
        def on_change_button_clicked(b):
-           ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
-           ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value  
+           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()
-           ai[0] = None if dropdown_circle.value == "人間" else dropdown_circle.value 
-           ai[1] = None if dropdown_cross.value == "人間" else dropdown_cross.value 
+           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)        元と同じなので省略

Marubatsu.play = play

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

gui_play()

変更ボタンとリセットボタンの処理の統合

変更ボタンリセットボタン行う処理違い は、ゲームのリセット行うか どうか だけ です。そのため、リセットボタンイベントハンドラ下記 のプログラムのように 修正 して 統合 することが できます

  • 5 行目ゲームのリセット後の処理変更ボタンイベントハンドラ処理と同じ なので、on_change_button_clicked呼び出す ように 修正 する
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              on_change_button_clicked(b)
元と同じなので省略

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')
        
        # それぞれの手番の担当を表す 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"),
        )
        
        # 〇 と × の 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)
  
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                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_reset_button_clicked(b):
            self.restart()
            on_change_button_clicked(b)
元と同じなので省略

Marubatsu.play = play

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

gui_play()

担当する AI の表示

AI選択 する Dropbox は、選択する項目変更 しても、変更ボタン または リセットボタン押さない限りゲーム反映 されることは ありません。そのため、Dropbox選択 されている 項目 と、手番担当する AI異なる場合あります

その状況 は、混乱の原因 となる 可能性がある ので、ゲーム盤 に、例えば「人間 VS ai1s」のような、実際 に行われている 対戦カード表示する ようにします。どのようにプログラムを修正すれば良いかについて少し考えてみて下さい。

対戦カード表示 するためには、play メソッドの 仮引数 ai情報必要 になりますが、ゲーム盤描画 する draw_board メソッドには その情報渡されていない ので、そのまま では 対戦カード表示 することは できません。そこで、下記 のプログラムのように、draw_board メソッドに、手番担当する AI情報を代入 する 仮引数ai追加 し、その情報 を使って ゲーム盤の下対戦カード表示 するようにします。

  • 4 行目:それぞれの 手番の担当文字列代入する変数空の list初期化 する
  • 5、6 行目繰り返し処理 を使って、それぞれの 手番の担当 を表す 文字列計算 し、names要素に追加 する
  • 7 行目ゲーム盤下部の座標指定 して、対戦カード表示 する。なお、この座標試行錯誤 して 調整したもの である
1  def draw_board(self, ax, ai):
元と同じなので省略
2      # 上部のメッセージを描画する
3      # 対戦カードの文字列を計算する
4      names = []
5      for i in range(2):
6          names.append("人間" if ai[i] is None else ai[i].__name__)
7      ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
元と同じなので省略
8           
9  Marubatsu.draw_board = draw_board 

行番号のないプログラム
def draw_board(self, ax, ai):
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn
    # 決着がついていれば勝者を表示する
    else:
        text = "winner " + self.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.BOARD_SIZE):
        ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            color = "red" if (x, y) == self.last_move else "black"
            self.draw_mark(ax, x, y, self.board[x][y], color) 
            
Marubatsu.draw_board = draw_board 
修正箇所
def draw_board(self, ax, ai):
元と同じなので省略
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
+   names = []
+   for i in range(2):
+       names.append("人間" if ai[i] is None else ai[i].__name__)
+   ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
元と同じなので省略
            
Marubatsu.draw_board = draw_board 

次に、draw_board呼び出し ている、play メソッドと play_loop メソッドを 修正 します。下記 は、修正 した play メソッドで、9 行目 で、draw_board実引数 ai追加 するという 修正 を行っています。

 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2          # ローカル関数としてイベントハンドラを定義する
 3          def on_mouse_down(event):
 4              # Axes の上でマウスを押していた場合のみ処理を行う
 5              if event.inaxes and self.status == Marubatsu.PLAYING:
 6                  x = math.floor(event.xdata)
 7                  y = math.floor(event.ydata)
 8                  self.move(x, y)                
 9                  self.draw_board(ax, ai)
10
11                  # 次の手番の処理を行うメソッドを呼び出す
12                  self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略
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')
        
        # それぞれの手番の担当を表す 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"),
        )
        
        # 〇 と × の 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)
  
        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_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)
+               self.draw_board(ax, ai)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
元と同じなので省略

Marubatsu.play = play

下記は、play_loop修正 で、修正箇所5、7 行目 です。

1  def play_loop(self, ai, ax, params, verbose, gui):   
元と同じなので省略
2              if gui:
3                  # AI どうしの対戦の場合は画面を描画しない
4                  if ai[0] is None or ai[1] is None:
5                      self.draw_board(ax, ai)
元と同じなので省略
6          if gui:
7              self.draw_board(ax, ai)
元と同じなので省略
8
9  Marubatsu.play_loop = play_loop
行番号のないプログラム
def play_loop(self, ai, ax, params, verbose, gui):   
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax, ai)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.draw_board(ax, ai)
        else:
            print(self)
            
    return self.status

Marubatsu.play_loop = play_loop
修正箇所
def play_loop(self, ai, ax, params, verbose, gui):   
元と同じなので省略
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
-                   self.draw_board(ax)
+                   self.draw_board(ax, ai)
元と同じなので省略
        if gui:
-           self.draw_board(ax)
+           self.draw_board(ax, ai)
元と同じなので省略

Marubatsu.play_loop = play_loop

上記修正後 に、下記 のプログラムで gui_play実行 すると、実行結果 のように ゲーム盤の下対戦カード表示される ようになります。また、Dropdown変更 して 変更ボタンリセットボタン押す と、対戦カード更新される ことを 確認 して下さい。

gui_play()

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

ゲームが決着した際の表示の工夫

現状 では、ゲーム決着しているか どうかが わかりづらい ので、ゲーム決着した場合 は、ゲームの画像背景色変更 することにします。Figure背景色 は、下記 のプログラムのように、Figureset_facecolor メソッドで 変更 できます。また、Axes登録 されている Figure は、Axesfigure 属性に 代入 されています。

本記事 では、ゲーム決着がついた場合背景色薄い黄色 を表す "lightyellow" にしますが、他の色が良い と思った人は 自由に変更 して下さい。

  • 6 行目ゲームの決着ついていない 場合は を、決着が ついている 場合は 薄い黄色 を表す 文字列を計算 する
  • 7 行目ax登録 されている Figure背景色6 行目計算した色変更 する
1  def draw_board(self, ax, ai):
元と同じなので省略
2      # 枠と目盛りを表示しないようにする
3      ax.axis("off")   
4    
5      # ゲームの決着がついていた場合は背景色を変える
6      facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
7      ax.figure.set_facecolor(facecolor)
元と同じなので省略
8            
9  Marubatsu.draw_board = draw_board 
行番号のないプログラム
def draw_board(self, ax, ai):
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を変える
    facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn
    # 決着がついていれば勝者を表示する
    else:
        text = "winner " + self.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.BOARD_SIZE):
        ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            color = "red" if (x, y) == self.last_move else "black"
            self.draw_mark(ax, x, y, self.board[x][y], color) 
            
Marubatsu.draw_board = draw_board 
修正箇所
def draw_board(self, ax, ai):
元と同じなので省略
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を変える
+   facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
+   ax.figure.set_facecolor(facecolor)
元と同じなので省略
            
Marubatsu.draw_board = draw_board 

上記修正後 に、下記 のプログラムで gui_play実行 し、ゲーム決着をつける と、実行結果 のように ゲームの画像背景色薄い黄色 になります。

gui_play()

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

引き分けの表示の変更

現状 では、引き分け になった場合に "winner draw" という メッセージ表示 されますが、勝者(winner)が draw という メッセージ変な気がする ので、下記 のプログラムのように、"Draw game"表示 するように 修正 します。あと、細かい修正 ですが、違和感があった ので、勝者メッセージ"winner"頭文字大文字に修正 しました。

  • 6、7 行目引き分け の場合の メッセージ"Draw game" にする
  • 10 行目勝者メッセージ"winner"頭文字大文字に修正 する
 1  def draw_board(self, ax, ai):
元と同じなので省略
 2      # ゲームの決着がついていない場合は、手番を表示する
 3      if self.status == Marubatsu.PLAYING:
 4          text = "Turn " + self.turn
 5      # 引き分けの場合
 6      elif self.status == Marubatsu.DRAW:
 7          text = "Draw game"
 8      # 決着がついていれば勝者を表示する
 9      else:
10          text = "Winner " + self.status
11      ax.text(0, -0.2, text, fontsize=20)
元と同じなので省略    
12            
13  Marubatsu.draw_board = draw_board 
行番号のないプログラム
def draw_board(self, ax, ai):
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # ゲームの決着がついていた場合は背景色を
    facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn
    # 引き分けの場合
    elif self.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.status
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.BOARD_SIZE):
        ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            color = "red" if (x, y) == self.last_move else "black"
            self.draw_mark(ax, x, y, self.board[x][y], color) 
            
Marubatsu.draw_board = draw_board 
修正箇所
def draw_board(self, ax, ai):
元と同じなので省略
    # ゲームの決着がついていない場合は、手番を表示する
    if self.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn
    # 引き分けの場合
+   elif self.status == Marubatsu.DRAW:
+       text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
-       text = "winner " + self.status
+       text = "Winner " + self.status
    ax.text(0, -0.2, text, fontsize=20)
元と同じなので省略    
            
Marubatsu.draw_board = draw_board 

上記修正後 に、下記 のプログラムで gui_play実行 し、引き分け にすると、実行結果 のように "Draw game"表示 されるようになります。実行結果は省略しますが、どちらかが勝利 した 場合 は、先頭"Winner" が表示 されることも 確認 できます。

gui_play()

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

CUI の場合の処理の確認

最後に、下記 のプログラムを 実行 し、CUI〇×ゲーム遊んだ場合正しい処理 が行われることを 確認 します。なお、実行結果は省略します。

mb.play(ai=[ai1s, ai1s])

今回の記事のまとめ

今回の記事は、以下の内容を行いました。

  • play メソッドの バグを修正 した
  • %matplotlib widgetマジックコマンドプログラムで実行 できるようにした
  • Dropdown選択した AI対戦が行われる ようにした
  • play メソッドに関する いくつかの改良 を行った

本記事で紹介した以外の play メソッドの 改良思いついた人 は、余裕があれば その 改良行ってみて下さい

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

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

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

なお、playplay_loopdraw_boarddocstring説明微妙に間違っていた ので 修正 しました。ただし、過去の記事修正 するのは 大変すぎる ので、過去の記事docstringそのまま にしてあります。

今回の記事では、util.py は変更していません。

次回の記事

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