0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その76 playメソッドのバグの修正とデフォルト引数に関する注意点

Last updated at Posted at 2024-04-28

目次と前回の記事

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

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

ルールベースの AI の一覧

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

play メソッドのバグ

前回の記事で、play メソッドを 実行 した際に 仮引数 ai代入 されている 値を元 に、Dropdown の項目自動的に設定 するプログラムを 記述 しましたが、その際に play メソッドには 重大なバグいくつか存在 するという説明を行いました。

バグその 1(人間の項目が複数作られる)

下記 は、play メソッドで 人間 VS AI対戦 を行うプログラムです。実行結果下図左 から、正しく動作 しているように 見えるかも しれませんが、Dropdown の上マウスを押す と、右下図 のように、人間の項目複数表示 されるという 問題が発生 することが 確認 できます。このようなことが起きる原因について少し考えてみて下さい。

%matplotlib widget
from marubatsu import Marubatsu

mb = Marubatsu()
mb.play(ai=[None, None], gui=True)

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

 

バグの原因の考察

下記は、play メソッドの中で、Dropdown の項目作成 する部分のプログラムです。

 1  # ai に代入されている内容を ai_list に追加する
 2  for i in range(2):
 3      # ai[i] が ai_values に登録済かどうかを判定する
 4      if ai[i] not in ai_values:
 5          # ラベルと項目の値を計算する
 6          if ai[i] is None:
 7              label = "人間"
 8              value = "人間"
 9          else:
10              label = ai[i].__name__        
11              value = ai[i]
12          # 項目を登録する
13          ai_list.append((label, value))
14          ai_values.append(value)
15          select_values.append(value)

下記 は、上記の中Dropdown の項目関する処理箇条書き にしたものです。

  • 4 行目ai[i]ai_values登録済か どうかを 判定 する
  • 8 行目人間の担当 の場合は、valueNone代入 する
  • 11 行目AI の担当 場合は、valueai[i]代入 する
  • 14 行目ai_valuesvalue追加 する

上記4 行目ai_values登録済であるか どうかを 判定 する際に使われている ai[i] と、15 行目ai_values追加 する、value に代入 されている が、人間担当する場合異なる点注目 して下さい。人間担当の場合 には ai_values"人間" が登録 されるため、次の繰り返し処理人間の手番 だった場合に、ai[i] in ai_valuesFalse になるので もう一度 15 行目で ai_values"人間" が登録 されてしまいます。

言葉だけの説明ではわかりづらいと思いますので、人間 VS 人間 の場合で 行われる処理 で、関連 する 変数の値どのように変化 するかを 下記の表示します。なお、条件式 は、その行条件式の値 を表します。表から上記 のプログラムを実行すると、ai_list人間の項目2 つ登録 されてしまうことが わかります

条件式 i ai[i] value ai_values ai_list
2 行目 0 None [] []
4 行目 False 0 None [] []
6 行目 False 0 None [] []
8 行目 0 None "人間" [] []
13 行目 0 None "人間" ["人間"] []
14 行目 0 None "人間" ["人間"] [("人間", "人間)]
2 行目 1 None ["人間"] [("人間", "人間)]
4 行目 False 1 None ["人間"] [("人間", "人間)]
6 行目 False 1 None ["人間"] [("人間", "人間)]
8 行目 1 None "人間" ["人間"] [("人間", "人間)]
13 行目 1 None "人間" ["人間", "人間"] [("人間", "人間)]
14 行目 1 None "人間" ["人間", "人間"] [("人間", "人間),
("人間", "人間)]

プログラムを 確認できる ように、コメントを除いた プログラムを 下記再掲 します。

 2  for i in range(2):
 4      if ai[i] not in ai_values:
 6          if ai[i] is None:
 7              label = "人間"
 8              value = "人間"
 9          else:
10              label = ai[i].__name__        
11              value = ai[i]
13          ai_list.append((label, value))
14          ai_values.append(value)
15          select_values.append(value)

本当 は、2 回目2、4、6 行目 では、value"人間"代入されたまま になっていますが、8 行目改めて value"人間"代入される ことを 明確するため空欄しました

バグの修正

この問題 は、4 行目条件式 で、ai_values実際登録する値使ってin 演算子判定を行う ことで 解決 することが できますそのため には、ai_values登録 する value計算 する 6 ~ 13 行目 にあった 処理 を、4 行目条件式より前に移動 する 必要あります下記 は、そのようplay メソッドを 修正 したプログラムです。

  • 9 ~ 14 行目16 行目if 文ブロックの中記述 されていた 処理 を、if 文ブロックの前移動 する
 1  import matplotlib.pyplot as plt
 2  import ipywidgets as widgets
 3  import math
 4
 5  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 6      # ai に代入されている内容を ai_list に追加する
 7      for i in range(2):
 8          # ラベルと項目の値を計算する
 9          if ai[i] is None:
10              label = "人間"
11              value = "人間"
12          else:
13              label = ai[i].__name__        
14              value = ai[i]
15          # value が ai_values に登録済かどうかを判定する
16          if value not in ai_values:
17              # 項目を登録する
18              ai_list.append((label, value))
19              ai_values.append(value)
20              select_values.append(value)
元と同じなので省略    
21
22  Marubatsu.play = play
行番号のないプログラム
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math

def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = ai[i].__name__        
            value = ai[i]
        # value が ai_values に登録済かどうかを判定する
        if value not in ai_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
            select_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        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)     

    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_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
+       if ai[i] is None:
+           label = "人間"
+           value = "人間"
+       else:
+           label = ai[i].__name__        
+           value = ai[i]
        # value が ai_values に登録済かどうかを判定する
        if value not in ai_values:
-           if ai[i] is None:
-               label = "人間"
-               value = "人間"
-           else:
-               label = ai[i].__name__        
-               value = ai[i]
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
            select_values.append(value)
元と同じなので省略    

Marubatsu.play = play

上記修正後 に、下記 のプログラムで play メソッドを 実行 すると、実行結果 のような エラーが発生 します。エラーが発生 する 原因 について少し 考えてみて下さい

mb.play(ai=[None, None], gui=True)

実行結果

略
     38 dropdown_cross = widgets.Dropdown(
     39     options=ai_list,
     40     description="×",
     41     layout=widgets.Layout(width="100px"),
     42     style={"description_width": "20px"},
---> 43     value=select_values[1],
     44 )       
略
IndexError: list index out of range

エラーの検証

エラーメッセージ から、---> 43 value=select_values[1], で、listインデックス範囲外(out of range)の 値が設定 されたこと が 原因 であることが わかります

先程同様 に、人間 VS 人間 の場合で行われる 処理 で、関連 する 変数の値どのように変化 するかを 下記示します

条件式 i ai[i] value ai_values select_values
1 行目 0 None [] []
2 行目 False 0 None [] []
4 行目 0 None "人間" [] []
8 行目 False 0 None "人間" [] []
10 行目 0 None "人間" ["人間"] []
11 行目 0 None "人間" ["人間"] ["人間"]
1 行目 1 None ["人間"] ["人間"]
2 行目 False 1 None ["人間"] ["人間"]
4 行目 1 None "人間" ["人間"] ["人間"]
8 行目 True 1 None "人間" ["人間"] ["人間"]

プログラムを 確認できる ように、コメントを除いた プログラムを 下記再掲 します。

 1  for i in range(2):
 2      if ai[i] is None:
 3          label = "人間"
 4          value = "人間"
 5      else:
 6          label = ai[i].__name__        
 7          value = ai[i]
 8      if value not in ai_values:
 9          ai_list.append((label, value))
10          ai_values.append(value)
11          select_values.append(value)

2 回目繰り返し8 行目条件式ai[i] not in ai_values から、value not in ai_values変わった ため True になる ので、上記の表 のように、select_values には、["人間"] という、要素1 つ しかない list代入 されます。そのためselect_values[1]参照 しようとすると、IndexError: list index out of range という エラーが発生 します。

select_values は、×両方の手番項目の値持つ必要あります上記 のプログラムの 問題点 は、11 行目select_valuesvalue追加する処理 が、8 行目if 文 によって valueai_values登録されていない場合 でしか 行われない点 です。従って、下記 のプログラムように、select_valuesvalue追加 する 処理8 行目前に移動 することで、この 問題を解決 することが できます

  • 12 行目14 行目if 文記述 されていた 処理if 文前に移動 する
 1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2      # ai に代入されている内容を ai_list に追加する
 3      for i in range(2):
 4          # ラベルと項目の値を計算する
 5          if ai[i] is None:
 6              label = "人間"
 7              value = "人間"
 8          else:
 9              label = ai[i].__name__        
10              value = ai[i]
11          # value を select_values に常に登録する
12          select_values.append(value)
13          # value が ai_values に登録済かどうかを判定する
14          if value not in ai_values:
15              # 項目を登録する
16              ai_list.append((label, value))
17              ai_values.append(value)
元と同じなので省略
18
19  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    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_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        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)     

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

Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
    # ai に代入されている内容を ai_list に追加する
    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_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
-         select_values.append(value)
元と同じなので省略

Marubatsu.play = play

上記修正後 に、下記 のプログラムで play メソッドを 実行 すると、実行結果 のように 正しい処理行われる ことが 確認 できます。

mb.play(ai=[None, None], gui=True)

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

 

バグその 2(前に表示された項目の内容が残る)

play メソッドには もう一つバグ がありますが、そのバグは 初心者 には 気づきづらい バグだと思います。具体的 には、上記で 人間 VS 人間対戦行った後 で、下記 のプログラムで AI VS AI対戦 を行うと、実行結果左下図 のように、一見 すると それぞれDropdown正しい AI選択 されているので、うまくいっている ように 見えるかもしれません が、Dropdown の上マウスを押す と、右下図 のように、今回対戦行っていない人間の項目表示 されるという 問題が発生 します。

from ai import ai1s, ai2s

mb.play(ai=[ai1s, ai2s], gui=True);

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

 

バグの原因の検証

この バグの原因Python初心者が気づく のは 難しい かもしれません。実は 筆者上記 のプログラムが 正しい と最初は 勘違い していました。この バグの原因 は、下記 のプログラムの 2 行目 のように、play メソッドの 最初 に、仮引数 ai_list値を表示 することで 確認 することができます。

def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    print("ai_list =", ai_list)
元と同じなので省略

Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    print("ai_list =", ai_list)
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    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_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        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)     

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

Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
+   print("ai_list =", ai_list)
元と同じなので省略

Marubatsu.play = play

上記の修正行った後 で、先程同じ順番 で、人間 VS 人間AI VS AI対戦 を行います。下記人間 VS 人間対戦行った場合 です。なお、実行結果 にはこの バグの検証必要print(ai_list)表示のみ表記 します。

mb.play(ai=[None, None], gui=True);

実行結果

ai_list = []

上記 のプログラムは、仮引数 ai_list対応 する 実引数記述していない ので、ai_list には デフォルト値 である []代入 され、実行結果表示 されます。

続けて 下記のプログラムで、AI VS AI対戦 を行います。

mb.play(ai=[ai1s, ai2s], gui=True);

実行結果

ai_list = [('人間', '人間')]

実行結果 から、先程と同様仮引数 ai_list対応する実引数記述していない にも かかわらずai_list[('人間', '人間')]代入されている ことが 確認 できます。これがDropdown人間の項目表示 されてしまう 原因 です。

この現象原因 については、本記事で デフォルト引数説明最初に行った 以前の記事説明 しましたが、〇×ゲーム実装する際そのような例 がこれまで 出てこなかったの で、ほとんどの方は忘れてしまったのではないかと思いますので、もう一度説明します。

デフォルト引数デフォルト値 を表す オブジェクト は、関数の定義実行 された 1 回だけ作成 され、デフォルト引数対応 する 実引数記述されなかった 場合は、そのオブジェクトデフォルト引数代入 されます。関数呼び出し行われた際 に、毎回 デフォルト値を表す オブジェクト新しく作成 される わけではない 点に 注意 して下さい。

従って、デフォルト値list などミュータブルオブジェクト設定 し、関数ブロックの中 で、デフォルト値要素値を変更 したり、要素を追加 するなどの 処理 を行うと、デフォルト値内容変化 します。

実際play メソッドでは、ai_list.append((label, value)) という、ai_listデフォルト値代入 されていた場合に、その 要素を追加 するという 処理行っています。そのため、ai_list対応 する 実引数記述せず人間 VS 人間対戦を行う と、デフォルト値空の list から、[("人間", "人間")]変化 してしまいます。そして、その値 が、AI VS AI対戦play メソッドで 行った際下記 のように 表示 されます。

ai_list = [('人間', '人間')]

この問題発生しない ようにするためには、下記 のプログラムのように、list などミュータブルな値デフォルト値したい場合 は、デフォルト値None を設定 し、関数ブロックの中 で、デフォルト引数の値None の場合に、デフォルト引数実際設定したい値代入 するという 処理記述する必要ありますこのように記述 することで、デフォルト引数対応 する 実引数記述しなかった 場合に、デフォルト引数新しい値毎回代入 されるので 上記 のような 問題発生しなくなります

  • 1 行目ai_listデフォルト値None修正 する
  • 3、4 行目ai_listNone の場合 に、ai_list空の list代入 する
 1  def play(self, ai, ai_list=None, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
 2      # ai_list が None の場合は、空の list で置き換える
 3      if ai_list is None:
 4          ai_list = []
元と同じなので省略
 5
 6  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=None, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # ai_list が None の場合は、空の list で置き換える
    if ai_list is None:
        ai_list = []
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    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_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        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)     

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

Marubatsu.play = play
修正箇所
-def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
+def play(self, ai, ai_list=None, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # ai_list が None の場合は、空の list で置き換える
+   if ai_list is None:
+       ai_list = []
元と同じなので省略

Marubatsu.play = play

上記修正後 に、下記 のプログラムで play メソッドを 実行 すると、実行結果 のように 正しい処理行われる ことが 確認 できます。人間 VS 人間実行結果省略 します。

なお、下記 のプログラムを 同じセルで実行 すると、2 つ の対戦の Dropdown の表示ゲーム盤よりも前並べて表示 されるので、別々のセル実行 して下さい。

mb.play(ai=[None, None], gui=True);
mb.play(ai=[ai1s, ai2s], gui=True);

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

 

デフォルト引数 params の修正

上記の問題 は、下記 のような 処理行う際発生 します。

  • デフォルト値list などミュータブルデータ代入 する
  • 関数ブロック内 で、デフォルト引数要素変更する処理行う

逆に言えば、上記 のような 処理行わない 場合は、上記の問題発生しません。例えば、play メソッドには、params=[{}, {}] のように、デフォルト値 に list のような ミュータブル設定 された デフォルト引数存在 しますが、params は、関数ブロックの中 で、その 要素変更 することは ない ので、このまま でも 問題発生しません

ただし、今後 params要素変更 するような 処理行う可能性ない とは 言い切れません し、要素変更することがない勘違い している場合が あるかもしれない ので、ミュータブルデータデフォルト値設定したい 場合は、特別な理由ない限り常にデフォルト値None設定 し、None の場合設定したい値代入する という 処理記述したほうが良い でしょう。

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

  • 1 行目paramsデフォルト値None修正 する
  • 6、7 行目paramsNone の場合に 元のデフォルト値代入 する
1  def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):      
2      # ai_list が None の場合は、空の list で置き換える
3      if ai_list is None:
4          ai_list = []
5      # params が None の場合のデフォルト値を設定する
6      if params is None:
7          params = [{}, {}]
元と同じなので省略
8
9  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_list が None の場合は、空の list で置き換える
    if ai_list is None:
        ai_list = []
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
   
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    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_values:
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        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)     

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

Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_list が None の場合は、空の list で置き換える
    if ai_list is None:
        ai_list = []
    # params が None の場合のデフォルト値を設定する
    if params is None:
        params = [{}, {}]
元と同じなので省略

Marubatsu.play = play

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

mb.play(ai=[None, None], gui=True)

上記の記事を記述している際に気づいたのですが、これまでは play_loopいくつか仮引数デフォルト値設定 していました。play_loop は、play メソッドの中から、必ず すべての仮引数対応 する 実引数を記述 して 呼び出している ので、デフォルト値設定 する 意味ありません。そこで、下記 プログラムのように、play_loopデフォルト引数 を、通常仮引数修正 することにします。

def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略

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)
                # 手番を人間が担当する場合は、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)
        else:
            print(self)
            
    return self.status   

Marubatsu.play_loop = play_loop
修正箇所
-def play_loop(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
+def play_loop(self, ai, ax, params, verbose, gui):
元と同じなので省略

Marubatsu.play_loop = play_loop

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

mb.play(ai=[None, None], gui=True)

細かい話 になりますが、上記 の「関数ブロック内 で、デフォルト引数内容変更する処理行う」は、その関数の中デフォルト値内容変更しなくてもデフォルト値他の関数呼び出し実引数に記述 し、呼び出された関数の中デフォルト値の内容変更する 場合も 含みます

例えば、下記 のプログラムは、a から 呼び出した 関数 b の中デフォルト値要素を追加 するので、a呼び出すたび に、xデフォルト値変化 します。

def a(x=[]):
    print(x)
    b(x)

def b(x):
    x.append(1)

a()
a()

実行結果

[]
[1]

GUI でゲームを遊ぶ関数の定義

前回の記事で、循環インポートエラーを解決 するために、ai1s ~ ai14sDropdown登録する処理play メソッドから 削除 したため、それらの AI を Dropdown選択できる ようにするためには、下記 のプログラムのように、ai1s ~ ai14sデータが登録 された ai_list作成する処理記述する必要あります

import ai

# Dropdown の作成の際に必要となる、AI のリストを作成する    
ai_list = []
for i in range(1, 15):
    ai_name = f"ai{i}s"  
    ai_list.append((ai_name, getattr(ai, ai_name)))   

mb.play(ai=[None, None], ai_list=ai_list, gui=True)

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

 

毎回 このような 処理を記述 するのは 大変 なので、GUIai1s ~ ai14s14 種類AI選択 する Dropdown表示 して 〇×ゲームを遊ぶ ための 下記 のような 関数を定義 することにします。

名前GUI遊ぶ(play)ので、gui_play とする
処理GUIai1s ~ ai14sAI選択 する Dropdown表示 して 〇×ゲームを遊ぶ
入力最初の担当 と、Dropdown表示する項目自由設定できる ように、play メソッドと 同じ意味 を持つ 仮引数 aiai_list持つ ようにする
出力:なし

gui_play の定義

下記 は、gui_play定義 です。なお、gui_play では、ai対応 する 実引数記述しなかった場合人間どうし対戦 が行われるように 工夫 してみました。

  • 1 行目gui_play には ai という 仮引数存在する ので、ai モジュール を再び ai_module という 別の名前インポート する 必要がある
  • 3 行目デフォルト値None設定 した デフォルト引数 aiai_list を持つ gui_play を定義する
  • 5、6 行目aiNone が代入 されていた場合は、ai[None, None]代入 することで、人間どうし対戦 が行われるようにする
  • 8 ~ 12 行目ai_listNone が代入 されていた場合は、ai_listai1s ~ ai14s項目 を持つ Dropdown作成するためデータ計算 して 代入 する
  • 14、15 行目Marubatsu クラスの インスタンスを作成 し、play メソッドで GUI上記の設定〇×ゲームの対戦行う
 1  import ai as ai_module
 2
 3  def gui_play(ai=None, ai_list=None):
 4      # ai が None の場合は、人間どうしの対戦を行う
 5      if ai is None:
 6          ai = [None, None]
 7      # ai_list が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
 8      if ai_list is None:
 9          ai_list = []
10          for i in range(1, 15):
11              ai_name = f"ai{i}s"  
12              ai_list.append((ai_name, getattr(ai_module, ai_name)))    
13
14      mb = Marubatsu()
15      mb.play(ai=ai, ai_list=ai_list, gui=True)
行番号のないプログラム
import ai as ai_module

def gui_play(ai=None, ai_list=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_list が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_list is None:
        ai_list = []
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_list.append((ai_name, getattr(ai_module, ai_name)))    
    
    mb = Marubatsu()
    mb.play(ai=ai, ai_list=ai_list, gui=True)

gui_play の動作の確認

下記 のプログラムで、gui_play実引数記述せず に呼び出した場合は、GUI人間 VS 人間対戦行われる ことが 確認 できます。

gui_play()

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

 

また、下記 のプログラムで、人間 VS ai1s対戦行われる ことが 確認 できます。

gui_play(ai=[None, ai1s])

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

 

また、下記 のプログラムで、ai1 のみ登録 された ai_list のデータを 作成 して キーワード引数記述 した場合でも gui_play正しく動作 することが 確認 できます。

gui_play(ai_list=[("ai1s", ai.ai1s)])

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

 

実は、gui_play には 問題あります。それが何か少し考えてみて下さい。

人間の項目の登録

gui_play には、下記 のプログラムのように、AI どうし対戦 を行う場合、実行結果右下図 のように、Dropdown人間の項目表示されない という 問題あります

gui_play(ai=[ai1s, ai1s])

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

 

先程gui_play()実行 した際に、下図 のように 人間の項目最後に表示 されたのは、人間 VS 人間対戦 が行われたため、play メソッドの 中でai_list登録されていない 人間の項目が 自動的登録された からです。

この問題 は、下記 のプログラムの 7 行目 のように、gui_play の中の、ai_list初期化処理 で、人間の項目データ代入 することで 解決 できます。

 1  def gui_play(ai=None, ai_list=None):
 2      # ai が None の場合は、人間どうしの対戦を行う
 3      if ai is None:
 4          ai = [None, None]
 5      # ai_list が None の場合は、人間と ai1s ~ ai14s の Dropdown を作成するためのデータを計算する  
 6      if ai_list is None:
 7          ai_list = [("人間", "人間")]
 8          for i in range(1, 15):
 9              ai_name = f"ai{i}s"  
10              ai_list.append((ai_name, getattr(ai_module, ai_name)))    
11    
12      mb = Marubatsu()
13      mb.play(ai=ai, ai_list=ai_list, gui=True)
行番号のないプログラム
def gui_play(ai=None, ai_list=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_list が None の場合は、人間と ai1s ~ ai14s の Dropdown を作成するためのデータを計算する  
    if ai_list is None:
        ai_list = [("人間", "人間")]
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_list.append((ai_name, getattr(ai_module, ai_name)))    
    
    mb = Marubatsu()
    mb.play(ai=ai, ai_list=ai_list, gui=True)
修正箇所
def gui_play(ai=None, ai_list=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_list が None の場合は、人間と ai1s ~ ai14s の Dropdown を作成するためのデータを計算する  
    if ai_list is None:
-       ai_list = []
+       ai_list = [("人間", "人間")]
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_list.append((ai_name, getattr(ai_module, ai_name)))    
    
    mb = Marubatsu()
    mb.play(ai=ai, ai_list=ai_list, gui=True)

上記修正後 に、下記 のプログラムで play メソッドを 実行 すると、実行結果 のように 正しい処理行われる ことが 確認 できます。なお、人間の項目gui_play の中最初に登録 したので、先程異なり人間の項目最初に表示 されるようになります。

gui_play(ai=[ai1s, ai1s])

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

 

ai1s ~ ai14s 以外の AI で対戦を行う場合の確認

下記 のプログラムで、gui_play を使って、ai1s ~ ai14s 以外AI対戦を行う 場合を 確認 します。実行結果 から、正しい処理 が行われることが 確認 できます。

from ai import ai1, ai2

gui_play(ai=[ai1, ai2])

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

 

gui_play の記述場所

gui_playMarubatsu クラスの メソッド として 定義しなかった 点が 気になってい る人が いるかもしれません が、gui_play の中ai モジュール関数利用している ので、gui_playMarubatsu クラスの メソッド として 定義 すると、前回の記事で説明したように、インポートの循環エラーが発生 してしまいます。

また、gui_play は、AI処理を行う 関数 ではない ので、ai モジュール中に記述 するのは 少し不自然 です。そこで、本記事では gui_playutil.py という 名前ファイルに保存する ことにします。util.py の中marubatsuai両方のモジュールインポート しても、インポートの循環起こらない ので エラー発生しません

なお、util とは、小規模 で、補助的機能 を持つ プログラム を表す ユーティリティ(utility)の で、比較的 良く使われるプログラミング用語 です。

dict を用いた Dropdown のラベルの設定

以前の記事で、Dropdownラベル設定する方法 として、options 属性(ラベル, 項目の値) という tuple要素 として持つ list設定 する方法を 紹介 しましたが、それ以外 にも { "ラベル": 項目の値 } という dict で設定 することが できます。また、dict で設定 することで、play メソッドの 処理より簡潔に記述 することが できるようになる ことに気が付きましたので、その方法を紹介することにします。

前回の記事では、ai の内容Dropdown自動登録 する 処理 を実装しましたが、その際に、ai_list の中 に、ai要素含まれているか判定する必要あります

ai_list要素 には、(ラベル, 項目の値) という tuple代入 されているため、in 演算子 を使って その判定 を行うことは できません。そのため、ai_list の中 から 項目の値取り出した list作成 して ai_values代入 するという 処理 を、下記 のプログラムのように リスト内包表記 を用いて行いました。

    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]

上記の処理 は、ai_list を、それぞれの項目{ "ラベル": 項目の値 } という キーキーの値 を持つ dict で設定 することで、簡潔に記述 することが できます

具体的には、下記 のプログラムのように、dictキーの値一覧 を表す list計算 する values という メソッド利用 します。

ai_values = ai_list.values()

ai_listdict で表現 することで、play メソッドを下記のように修正します。なお、ai_list という 名前のまま では なので、ai_dict という 名前に変更 しました。

  • 1 行目:仮引数 ai_listai_dict変更 する
  • 4 行目ai_dictNone の場合に 代入する値空の dict修正 する
  • 14 行目:この下にあった ai_values計算 する 処理削除 する
  • 18 行目ai_values の代わりに、ai_dict.values()判定を行う ように 修正 する
  • 20 行目ai_dictlabelキーの値value代入 する
  • 21 行目:この下にあった ai_values項目を登録 する 処理削除 する
  • 3、25、32 行目ai_listai_dict変更 する
 1  def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
 2      # ai_dict が None の場合は、空の list で置き換える
 3      if ai_dict is None:
 4          ai_dict = {}
元と同じなので省略
 5      # ai に代入されている内容を ai_dict に追加する
 6      for i in range(2):
 7          # ラベルと項目の値を計算する
 8          if ai[i] is None:
 9              label = "人間"
10              value = "人間"
11          else:
12              label = ai[i].__name__        
13              value = ai[i]
14          # この下にあった、ai_values を計算する処理を削除する
15          # value を select_values に常に登録する
16          select_values.append(value)
17          # value が ai_values に登録済かどうかを判定する
18          if value not in ai_dict.values():
19              # 項目を登録する
20              ai_dict[label] = value
21          # この下にあった、ai_values に value を登録する処理を削除する
22
23      # 〇 と × の Dropdown を作成する   
24      dropdown_circle = widgets.Dropdown(
25          options=ai_dict,
26          description="",
27          layout=widgets.Layout(width="100px"),
28          style={"description_width": "20px"},
29          value=select_values[0],
30      )
31      dropdown_cross = widgets.Dropdown(
32          options=ai_dict,
33          description="×",
34          layout=widgets.Layout(width="100px"),
35          style={"description_width": "20px"},
36          value=select_values[1],
37      )   
元と同じなので省略
38
39  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)
    
    # それぞれの手番の担当を表す 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)     

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

Marubatsu.play = play
修正箇所
-def play(self, ai, ai_list=None, params=None, verbose=True, seed=None, gui=False, size=3):      
+def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):      
    # ai_dict が None の場合は、空の list で置き換える
-   if ai_list is None:
+   if ai_dict is None:
-       ai_list = []
+       ai_dict = {}
元と同じなので省略
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = ai[i].__name__        
            value = ai[i]
-       ai_values = [value for label, value in ai_list]
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
-       if value not in ai_values:
+       if value not in ai_dict.values():
            # 項目を登録する
-           ai_list.append((label, value))
+           ai_dict[label] = value
-           ai_values.append(value)

    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
-       options=ai_list,
+       options=ai_dict,
        description="",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
-       options=ai_list,
+       options=ai_dict,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[1],
    )     
元と同じなので省略

Marubatsu.play = play

次に、gui_playai_dict作成する処理 を、下記 のように 修正 します。

  • 1 行目:仮引数 ai_listai_dict変更 する
  • 7 行目ai_dict に、人間の項目 を表す dict代入 するように 修正 する
  • 10 行目ai_dictai_nameキーの値 に、getattr計算 した AI の関数代入 する
  • 6、13 行目ai_listai_dict変更 する
 1  def gui_play(ai=None, ai_dict=None):
 2      # ai が None の場合は、人間どうしの対戦を行う
 3      if ai is None:
 4          ai = [None, None]
 5      # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
 6      if ai_dict is None:
 7          ai_dict = { "人間": "人間"}
 8          for i in range(1, 15):
 9              ai_name = f"ai{i}s"  
10              ai_dict[ai_name] = getattr(ai_module, ai_name)
11    
12      mb = Marubatsu()
13      mb.play(ai=ai, ai_dict=ai_dict, gui=True)
行番号のないプログラム
def gui_play(ai=None, ai_dict=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": "人間"}
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = getattr(ai_module, ai_name)
    
    mb = Marubatsu()
    mb.play(ai=ai, ai_dict=ai_dict, gui=True)
修正箇所
-def gui_play(ai=None, ai_list=None):
+def gui_play(ai=None, ai_dict=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
-   if ai_list is None:
+   if ai_dict is None:
-       ai_list = [("人間", "人間")]
+       ai_dict = { "人間": "人間"}
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
-           ai_list.append((ai_name, getattr(ai_module, ai_name)))               
+           ai_dict[ai_name] = getattr(ai_module, ai_name)
    
    mb = Marubatsu()
-   mb.play(ai=ai, ai_list=ai_list, gui=True)
+   mb.play(ai=ai, ai_dict=ai_dict, gui=True)

上記修正後 に、下記 のプログラムで、gui_play実引数記述せず に呼び出した場合は、GUI人間 VS 人間対戦正しく行われる ことが 確認 できます。

gui_play()

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

 

本記事では省略しますが、余裕がある方 は先程行った gui_play(ai=[ai1s, ai1s]) など実行 して gui_play正しく動作する ことを 確認 してみて下さい。

今回の記事のまとめ

今回の記事では、play メソッドの バグの修正 を行いました。また、その際に、デフォルト引数デフォルト値list などミュータブルデータ代入 した場合の 注意点対処法 について 説明 しました。

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

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

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

以下のリンクは、今回の記事で作成した 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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?