0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その114 データ構造を変更した結果生じた Marubatsu_GUI クラスのバグの修正

Last updated at Posted at 2024-09-08

目次と前回の記事

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

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

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
test.py テストに関する関数
util.py ユーティリティ関数の定義。現在は gui_play のみ定義されている
tree.py ゲーム木に関する Node、Mbtree クラスの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

Marubatsu_GUI クラスのバグの修正と改良

前回の記事で Marubatsu_GUI に対して修正を行った結果、いくつかのバグが発生しているので、今回の記事ではそれらのバグの修正といくつかの改良を行うことにします。

今回の記事で紹介するようなバグは、プログラムの データ構造などを後から変更する 場合などで良く発生するバグです。特に、本記事で行っているように、その場の思い付きでプログラムを改良していく という、日曜大工的なプログラミングにはつきもののバグ でしょう。

gui_play にキーワード引数 ai を記述した場合のバグ

最初のバグは、普通に gui_play() を呼び出しているだけでは気づくこと難しいバグだと思います。筆者も最初はこのバグに気づかずに、様々な条件で gui_play を呼び出した際にこのバグ発見しました。

バグが発生する状況

具体的には、gui_play にキーワード引数 ai を記述して、最初に特定の AI どうしの対戦を行う ようにし、その後で リセットボタンをクリックする とエラーが発生します。

例えば、下記のプログラムで gui_play で最初に ai2s VS ai2s どうしの対戦を行うと、実行結果のように正しく対戦が行われます。なお、対戦する AI はどの AI でもかまいません。

from util import gui_play
from ai import ai2s

gui_play(ai=[ai2s, ai2s])

実行結果(実行結果はランダムなので下記と異なる場合があります)

しかし、その後でリセットボタンをクリックしてもう一度 ai2s VS ai2s どうしの対戦を行おうとすると、下記のようなエラーが発生します。

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File c:\Users\ys\ai\marubatsu\114\marubatsu.py:776, in Marubatsu_GUI.create_event_handler.<locals>.on_reset_button_clicked(b)
    774 self.mb.restart()
    775 self.output.clear_output()
--> 776 on_change_button_clicked(b)

File c:\Users\ys\ai\marubatsu\114\marubatsu.py:765, in Marubatsu_GUI.create_event_handler.<locals>.on_change_button_clicked(b)
    763 def on_change_button_clicked(b):
    764     for i in range(2):
--> 765         self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
    766                                                 else self.dropdown_list[i].value
    767     self.mb.play_loop(self, self.params)

TypeError: cannot unpack non-iterable function object

1 回目ai2s VS ai2s正しく動作する が、リセットボタンを押した後の 2 回目の対戦ではエラーが発生する 点が不思議だと思っている人が多いかもしれません。このエラーの原因について少し考えてみて下さい。

1 回目の ai2s VS ai2s でエラーが発生しない理由の検証

上記のエラーメッセージから、下記のプログラムを実行した際に「反復可能オブジェクトでない(non-iterable)関数オブジェクト(function object)を展開(unpack)することはできない」というメッセージが表示されたことがわかります。

self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                           else self.dropdown_list[i].value

ai2s VS ai2s は AI どうしの対戦なので、self.dropdown_list[i].value == "人間"True になることはないはずです。そのため、上記のプログラムでは下記の処理が行われた可能性が高く、Dropdown の 選択中の項目の値 を表す self.dropdown_list[i].value関数が代入されている可能性が高い ことがわかります。

self.mb.ai[i], self.params[i] = self.dropdown_list[i].value

また、下記のエラーメッセージから、エラーが発生するまでの間に下記の処理が行われたことが確認できます。

  • create_event_handler 内で定義された、リセットボタンがクリックされた際に呼び出される on_reset_button_clicked が呼び出された
  • その中で on_change_button_clicked(b) が呼び出された
  • on_chage_button_clicked の処理の中でエラーが発生した
File c:\Users\ys\ai\marubatsu\114\marubatsu.py:776, in 
     Marubatsu_GUI.create_event_handler.<locals>.on_reset_button_clicked(b)
    774 self.mb.restart()
    775 self.output.clear_output()
--> 776 on_change_button_clicked(b)

1 回目の ai2s VS ai2s の対戦の処理は、gui_play を実行した後ですぐに完了し、その際にリセットボタンや変更ボタンのクリックは行われないので、先程のエラーの原因となった on_reset_button_clickedon_reset_button_clicked も実行されません。これが、1 回目 の ai2s VS ai2s でエラーが発生しない理由です。

2 回目の ai2s VS ai2s でエラーが発生する理由の検証

先程示したように、 2 回目の ai2s VS ai2s では、下記の処理によってエラーが発生した可能性が高いです。また、その際に Dropdown を変更する操作は行っていないので、選択中の Dropdown の項目の値を表す self.dropdown_list[i].value に代入された値は、Dropdown を作成してから変更されていない可能性が高い ことがわかります。

self.mb.ai[i], self.params[i] = self.dropdown_list[i].value

そこで、下記のプログラムのように、Dropdown を作成する create_dropdown 内で、作成した Dropdown の value 属性の値を print で表示してみることにします。

  • 14 行目:11 行目から、作成する Dropdown の value 属性 には select_values[i] を代入している ことがわかるので、その値を print で表示する ようにする
 1  from marubatsu import Marubatsu_GUI
 2  import ipywidgets as widgets
 3
 4  def create_dropdown(self):
元と同じなので省略
 5          self.dropdown_list.append(
 6              widgets.Dropdown(
 7                  options=self.ai_dict,
 8                  description=description,
 9                  layout=widgets.Layout(width="100px"),
10                  style={"description_width": "20px"},
11                  value=select_values[i],
12              )
13          )
14          print(select_values[i])   
15
16  Marubatsu_GUI.create_dropdown = create_dropdown
行番号のないプログラム
from marubatsu import Marubatsu_GUI
import ipywidgets as widgets

def create_dropdown(self):
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 〇 と × の Dropdown を格納する list
    self.dropdown_list = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
            value = "人間"
        else:
            label = self.mb.ai[i].__name__        
            value = self.mb.ai[i]
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value
    
        # Dropdown の description を計算する
        description = "" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )
        print(select_values[i])   

Marubatsu_GUI.create_dropdown = create_dropdown
修正箇所
from marubatsu import Marubatsu_GUI
import ipywidgets as widgets

def create_dropdown(self):
元と同じなので省略
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )
+       print(select_values[i])   

Marubatsu_GUI.create_dropdown = create_dropdown

上記の修正後に下記のプログラムを実行すると、実行結果の function ai2s という表示から select_values[i]ai2s という関数が代入されている ことが確認できます。なお、Dropdown は 〇 と × の手番の 2 つを作成し、どちらも ai2s を選択状態にするので 2 回表示されます。また、ゲーム盤を表す画像は元と同じなので省略します。

gui_play(ai=[ai2s, ai2s])

実行結果(at の右の数字は毎回変わります)

<function ai2s at 0x00000205B8D46660>
<function ai2s at 0x00000205B8D46660>

Dropdown の value 属性に ai2s の関数のみが代入されている ことが確認できたので、create_dropdown の中で select_values[i] を計算する処理を探す と、その処理は下記のプログラムのように記述されていることが確認できます。

下記のプログラムでは、手番の担当が人間の場合は 4 行目で "人間" という文字列が、担当が AI の場合 は 7 行目で AI の関数が value に代入 され、9 行目で select_valuesvalue の値が追加されます。

1  # ラベルと項目の値を計算する
2  if self.mb.ai[i] is None:
3      label = "人間"
4      value = "人間"
5  else:
6      label = self.mb.ai[i].__name__        
7      value = self.mb.ai[i]
8  # value を select_values に常に登録する
9  select_values.append(value)

このことから、エラーの原因が、「前回の記事Dropdown の項目の値を ( AI の関数, AI のパラメーター ) という tuple に変更 したにも関わらす、Dropdown に最初に選択される項目の値をそのような tuple に変更するのを忘れてしまったため」であることがわかります。

従って、create_dropdown を下記のプログラムのように修正することで、このエラーを修正することができます。

  • 6 行目:人間が担当する場合はパラメーターは存在しないのでパラメーターを表す要素に 空の dict を代入した tuple に変更する
  • 9 行目:AI が担当する場合は、パラメーターは self.params[i] に代入されているので、それをパラメーターを表す要素に代入した tuple に変更する
 1  def create_dropdown(self):
元と同じなので省略
 2      for i in range(2):
 3          # ラベルと項目の値を計算する
 4          if self.mb.ai[i] is None:
 5              label = "人間"
 6              value = ( "人間", {} )
 7          else:
 8              label = self.mb.ai[i].__name__        
 9              value = ( self.mb.ai[i], self.params[i] )
10          # value を select_values に常に登録する
11          select_values.append(value)
元と同じなので省略
12
13  Marubatsu_GUI.create_dropdown = create_dropdown
行番号のないプログラム
def create_dropdown(self):
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 〇 と × の Dropdown を格納する list
    self.dropdown_list = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
            value = ( "人間", {} )
        else:
            label = self.mb.ai[i].__name__        
            value = ( self.mb.ai[i], self.params[i] )
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value
    
        # Dropdown の description を計算する
        description = "" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )
        print(select_values[i])   

Marubatsu_GUI.create_dropdown = create_dropdown
修正箇所
def create_dropdown(self):
元と同じなので省略
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
-           value = "人間"
+           value = ( "人間", {} )
        else:
            label = self.mb.ai[i].__name__        
-           value = self.mb.ai[i]
+           value = ( self.mb.ai[i], self.params[i] )
        # value を select_values に常に登録する
        select_values.append(value)
元と同じなので省略

Marubatsu_GUI.create_dropdown = create_dropdown

上記の修正後に下記のプログラムを実行すると、実行結果のように select_values[i](ai2s, {}) という tuple が代入されている ことが確認できます。また、リセットボタンをクリックしてもエラーが発生しなくなることが確認できます。

gui_play(ai=[ai2s, ai2s])

実行結果(at の右の数字は毎回変わります)

(<function ai2s at 0x00000205B8D46660>, {})
(<function ai2s at 0x00000205B8D46660>, {})

Dropdown で人間を選択した際のバグ

上記の修正でバグが完全に修正されたと思った人がいるかもしれませんが、実は別のバグがまだ潜んでいます。それは、上記の gui_play を実行後に、どちらかの Dropdown に「人間」を選択してリセットボタンをクリックする と、下記のような エラーが発生する というものです。このエラーの原因について少し考えてみて下さい

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File c:\Users\ys\ai\marubatsu\114\marubatsu.py:776, in Marubatsu_GUI.create_event_handler.<locals>.on_reset_button_clicked(b)
    774 self.mb.restart()
    775 self.output.clear_output()
--> 776 on_change_button_clicked(b)

File c:\Users\ys\ai\marubatsu\114\marubatsu.py:767, in Marubatsu_GUI.create_event_handler.<locals>.on_change_button_clicked(b)
    764 for i in range(2):
    765     self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
    766                                             else self.dropdown_list[i].value
--> 767 self.mb.play_loop(self, self.params)

File c:\Users\ys\ai\marubatsu\114\marubatsu.py:397, in Marubatsu.play_loop(self, mb_gui, params)
    395 # ai が着手を行うかどうかを判定する
    396 if ai[index] is not None:
--> 397     x, y = ai[index](self, **params[index])
    398 else:
    399     # キーボードからの座標の入力
    400     coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")

TypeError: 'str' object is not callable

エラーの原因の検証

エラーは下記のプログラムを実行した際に発生し、エラーメッセージは文字列型(str)のオブジェクトは呼び出すことができない(not callable)という意味を表します。

x, y = ai[index](self, **params[index])

従って、ai[index] に関数ではなく 文字列型のデータが代入されている可能性が高い ことがわかります。そこでエラーメッセージをさかのぼって ai[index] がどのように計算されるかを確認 すると、その処理は下記のエラーメッセージに記述されていることがわかります。

File c:\Users\ys\ai\marubatsu\114\marubatsu.py:767, in Marubatsu_GUI.create_event_handler.<locals>.on_change_button_clicked(b)
    764 for i in range(2):
    765     self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
    766                                             else self.dropdown_list[i].value
--> 767 self.mb.play_loop(self, self.params)

下記は上記のエラーメッセージから ai に値を代入する処理を抜き出したものです。

self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                           else self.dropdown_list[i].value

この処理は、ai[i] に下記のような値を計算して代入するという処理を意図したものです。

  • Dropdown に選択されている項目の値が "人間" の場合は None を代入する
  • そうでなければ self.dropdown_list[i].value に代入された tuple の 最初の要素を代入 する

一見すると上記のプログラムで正しい処理を行うことができるように見えるかもしれませんが、上記のプログラムは間違っています。どこが間違っているかを少し考えてみて下さい。

間違いは、self.dropdown_list[i].value には tuple が代入されている にも関わらず、self.dropdown_list[i].value == "人間" という条件式で Dropdown に選択されている項目の値が "人間" であるかを判定している点 です。

Dropdown に 人間が選択されている場合self.dropdown_list[i].value には ( "人間", {} ) という tuple が代入されている ので、条件式は tuple の 最初の要素と "人間" を比較 する self.dropdown_list[i].value[0] == "人間" のように記述する必要があります。

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

 1  from marubatsu import Marubatsu
 2  import math
 3  import pickle
 4  from datetime import datetime
 5  from tkinter import Tk, filedialog
 6
 7  def create_event_handler(self):
元と同じなので省略
 8      # 変更ボタンのイベントハンドラを定義する
 9      def on_change_button_clicked(b):
10          for i in range(2):
11              self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
12                                                         else self.dropdown_list[i].value
13          self.mb.play_loop(self, self.params)
元と同じなので省略
14    
15  Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
from marubatsu import Marubatsu
import math
import pickle
from datetime import datetime
from tkinter import Tk, filedialog

def create_event_handler(self):
    # 乱数の種のチェックボックスのイベントハンドラを定義する
    def on_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b=None):
        path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                        initialdir="save")
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                change_step(data["move_count"])
                for i in range(2):
                    value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
                    self.dropdown_list[i].value = value               
                if data["seed"] is not None:                   
                    self.checkbox.value = True
                    self.inttext.value = data["seed"]
                else:
                    self.checkbox.value = False
                    
    def on_save_button_clicked(b=None):
        name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                for i in range(2)]
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{name[0]} VS {name[1]} {timestr}"
        path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                            initialdir="save", initialfile=fname,
                                            defaultextension="mbsav")
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
                
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

マスの上でクリックすることで着手を行う。
下記の GUI で操作を行うことができる。
()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。

乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
開く(-,L)\tファイルから対戦データを読み込む
保存(+,S)\tファイルに対戦データを保存する
?(*,H)\t\tこの操作説明を表示する
手番の担当\tメニューからそれぞれの手番の担当を選択する
\t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
変更\t\tゲームの途中で手番の担当を変更する
リセット\t手番の担当を変更してゲームをリセットする
待った(0)\t1つ前の自分の着手をキャンセルする
<<(↑)\t\t最初の局面に移動する
<(←)\t\t1手前の局面に移動する
>(→)\t\t1手後の局面に移動する
>>(↓)\t\t最後の着手が行われた局面に移動する
スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する

手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    self.help_button.on_click(on_help_button_clicked)
    
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
                                                       else self.dropdown_list[i].value
        self.mb.play_loop(self, self.params)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        self.output.clear_output()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.update_gui()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    # step 手目の局面に移動する
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.update_gui()        

    def on_first_button_clicked(b=None):
        change_step(0)

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

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        if self.mb.move_count != changed["new"]:
            change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            with self.output:
                self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self, self.params)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
            "-": on_load_button_clicked,            
            "l": on_load_button_clicked,            
            "+": on_save_button_clicked,            
            "s": on_save_button_clicked,            
            "*": on_help_button_clicked,            
            "h": on_help_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                event.inaxes = True
                event.xdata = num % 3
                event.ydata = 2 - (num // 3)
                on_mouse_down(event)
            except:
                pass
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)       
    
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
from marubatsu import Marubatsu
import math
import pickle
from datetime import datetime
from tkinter import Tk, filedialog

def create_event_handler(self):
元と同じなので省略
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
-           self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
+           self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
                                                       else self.dropdown_list[i].value
        self.mb.play_loop(self, self.params)
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、どちらかの担当を「人間」に変更した後でリセットボタンを押した際にエラーが発生しなくなったことを確認して下さい。

gui_play(ai=[ai2s, ai2s])

修正前のプログラムで上記のエラーが発生しないというバグ

上記で修正した、Dropdown の選択を人間に選び直した際 に発生するエラーは、実は今回の記事の 修正を行う前のプログラムでは発生しません。興味がある方は、JupyterLab を再起動 して今回行った修正をなかったことにした後で、下記の手順の作業を行ってみて下さい。エラーは発生せず、問題なく対戦を行うことができる はずです。なお、今回の記事の marubatsu.ipynb では下記の作業は行いません。

  • from util import gui_play を実行して今回の記事の修正を行う前の gui_play をインポートし、gui_play() を実行する
  • いずれかの Dropdown にいずれかの AI を選択後に変更ボタンをクリックする
  • その後で再び Dropdown に 人間選択しなおして 変更ボタンをクリックする

gui_playai_dict に代入した 人間の項目に対応するキー の値には、下記のプログラムのように tuple が代入される ので、上記の操作で Dropdown に 人間を選択しなおす と、Dropdown の value 属性には ("人間", {}) という tuple が代入されるはず です。

ai_dict = { "人間": ("人間", {}) }

そのため、今回の記事で先程行った 修正を行う前のプログラム では、変更ボタンをクリックした際に行われる 下記の処理で エラーが発生するはず なのですが、実際にはエラーは発生しません。何故エラーが発生しないかについて少し考えてみて下さい。

self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                           else self.dropdown_list[i].value

なお、エラーが発生しないことは良いことであると思う人がいるかもしれませんが、その考え方は間違っています発生するべきエラーが発生しない という現象は、エラーが発生するよりも 良くないことです

例えば、痛みは体の中の不調を知らせるものです。体の中に不調があるにも関わらず、痛みを感じない ということは、不調の存在を見過ごす ことになってしまうので決して 良いことであるとは言えません

それと同様に、バグが存在するにもかかわらず、そのことが エラーなどで明るみに出ない ということは、決して良いことではないのです

病気を早期に発見して対処することが重要であるのと同様に、プログラムのバグも なるべく早く発見して対処することが重要 です。病気と同様に、バグを見過ごしてしまうと、後でそのバグが発見された時の修正の作業が大変 になるからです。

残念ながらそのような症状がなかなか明るみに出ないバグは、よくあるバグの一つで、一般的に発見と修正が困難です。このバグを詳しく説明しているのは、バグの発見と修正の技術の向上には様々なバグを経験することが重要だからです。

エラーが発生しない原因の検証

エラーが発生しない原因は、create_dropdown 内の下記のプログラムにあります。なお、下記は 今回の記事で修正を行う前 のプログラムである点に注意して下さい。

忘れている人が多いと思いますのでおさらいしますが、13 ~ 15 行目の処理 は、gui_play を呼び出す際に キーワード引数 ai を記述して指定した AI がDropdown の項目に含まれていない場合 に、その AI を Dropdown の 項目に登録するための処理 です。例えば gui_playai2s のように、ルールベースの AI に関しては名前の最後に s がつく AI を Dropdown に登録しますが、ai1 のような s がつかない AI は登録しません。13 ~ 15 行目は gui_play(ai=[ai1, ai1]) のように、s がつかない AI で対戦を行った場合に、Dropdown に ai1 を登録する処理です。

 1  def create_dropdown(self):
元と同じなので省略
 2      for i in range(2):
 3          # ラベルと項目の値を計算する
 4          if self.mb.ai[i] is None:
 5              label = "人間"
 6              value = "人間"
 7          else:
 8              label = self.mb.ai[i].__name__        
 9              value = self.mb.ai[i]
10          # value を select_values に常に登録する
11          select_values.append(value)
12          # value が ai_values に登録済かどうかを判定する
13          if value not in self.ai_dict.values():
14              # 項目を登録する
15              self.ai_dict[label] = value

キーワード引数 ai を記述せずに gui_play() を実行した場合は、下記の手順で処理が行われます。

  • gui_play() のように キーワード引数 ai を記述せずに呼び出した場合は、最初に 人間 VS 人間 の対戦が行われる
  • 人間が担当する場合は self.mb.ai[i] には None が代入されるので、6 行目が実行されて labelvalue"人間" という文字列が代入される
  • 13 行目で、self.ai_dict のキーの値に value に代入された "人間" が存在しないことを判定する
  • ai_dict"人間" というキーに対応するキーの値は ( "人間", {} ) という tuple なので、"人間" というキーの値は存在しない。
  • 13 行目の条件式が True になるので、15 行目が実行され、ai_dict"人間" のキーの値( "人間", {} ) から "人間" に上書きされる

上記の処理が行われた結果、Dropdown に 人間を選択するとdropdown_list[i].value には ( "人間", {} ) ではなく、"人間" が代入されるようになります

self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value == "人間" \
                                           else self.dropdown_list[i].value

そのため、上記のプログラムが実行された際には、下記のプログラムの処理が実行され、self.mb.ai[i] には人間が担当することを表す None が、self.params[i] にはパラメーターが存在しないことを表す {} が正しく代入されます。

self.mb.ai[i], self.params[i] = (None, {})

このバグの性質のまとめと注意点

以上が、今回の記事の最初で説明したバグが存在するにも関わらず、正しい処理が行われてしまう理由ですが、このことを別の言葉で説明すると以下のようになります。

  • プログラムに バグが 2 つ存在する1
  • それぞれのバグによって間違った処理が行われる
  • それぞれのバグを バグ A と バグ B と表記すると、バグ A によって行われた間違った処理の結果を使ってバグ B が間違った処理を行った結果、偶然正しい処理が行われてしまった

まだわかりづらいかもしれませんので、現実の例で例えると以下のようになります。

  • 直方体の体積を計算する際に、底面の長方形の面積を間違って「縦 × 横 ÷ 2」という式で計算した2
  • 体積を計算するために間違って「底面の面積 × 高さ × 2」で計算した3

上記の手順で行われた 2 つの計算は、いずれも間違っていますが、2 つの間違いを重ねた結果偶然に正しく 直方体の体積が 計算されてしまいます

途中の計算が間違っていても、正しい答えが得られるのだから結果オーライだと思う人がいるかもしれませんが、例えば同じプログラムの中で、上記の間違った長方形の面積の計算方法を使って、直方体の表面積を計算すると 間違った答えが計算されてしまいます。このようなバグを放置すると、その場はうまく行くように見えるかもしれませんが、後で予期しない別のバグが発生する原因になる ので、決して放置するべきではありません

このように、複数のバグが行った処理 によって 偶然正しい処理が行われてしまう というバグはかなり質の悪いバグで、バグの発見や修正が困難です。今回の記事で示したように 実際に発生する場合がある ので紹介しました。

なお、JupyterLab を再起動した方は、もう一度今回の記事で入力したバグを修正したプログラムを実行しなおしておいてください。

人間を担当する際のデータ構造の修正

手番の担当が人間であるかどうかによって、下記のプログラムのような処理を行うのがわかりづらく、面倒だと思った人はいないでしょうか。

self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
                                           else self.dropdown_list[i].value

かなり前の記事で説明したので忘れた方が多いのではないかと思いますが、このような処理を行っているのは、選択中の Dropdown の項目の値を表す value 属性に None が代入されている場合 は、どの項目も選択されていないという意味を表す ため、人間の項目の値に None を設定することができず、None に代わる別の値として "人間" という文字列を設定するという苦肉の策を行ったからです。忘れた方は復習して下さい。

しかし、前回の記事で Dropdown の 項目の値( AI の関数, AI のパラメーター ) という tuple に変更した ので、人間を表す項目の値( "人間", {} ) ではなく、( None, {} ) のように 変更することができる ようになっています。また、そのように変更することで、上記のプログラムを下記のように 簡潔に記述 することができるようになります。

self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
修正箇所
-self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
-                                           else self.dropdown_list[i].value
+self.mb.ai[i], self.params[i] = self.dropdown_list[i].value

そこで、人間が担当する際のデータ構造を ( "人間", {} ) という tuple から ( None, {} ) という tuple に変更することにします。

gui_play の修正

下記は gui_play の修正です。

  • 7 行目:人間の項目に対応する項目の値を ( None, {} ) に修正する
1  import ai as ai_module
2  from ai import ai_gt6
3  from util import load_bestmoves
4
5  def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
6      if ai_dict is None:
7          ai_dict = { "人間": ( None, {} ) }
元と同じなので省略
行番号のないプログラム
import ai as ai_module
from ai import ai_gt6
from util import load_bestmoves

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
    # ai が None の場合は、人間どうしの対戦を行う
    if ai is None:
        ai = [None, None]
    if params is None:
        params = [{}, {}]
    # ai_dict が None の場合は、ai1s ~ ai14s の Dropdown を作成するためのデータを計算する
    if ai_dict is None:
        ai_dict = { "人間": ( None, {} )}
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_dict[ai_name] = (getattr(ai_module, ai_name), {})
        bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
        ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
    
    print(ai_dict)
    mb = Marubatsu()
    mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)
修正箇所
import ai as ai_module
from ai import ai_gt6
from util import load_bestmoves

def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
    if ai_dict is None:
-       ai_dict = { "人間": ( "人間", {} ) }
+       ai_dict = { "人間": ( None, {} ) }
元と同じなので省略

create_dropdown の修正

下記は create_dropdown の修正です。

  • 4 ~ 8 行目:人間が担当する場合の項目の値を ( None, {} ) に変更したことで、項目の値は 人間でも AI でも 同じ ( self.mb.ai[i], self.params[i] ) で表現できるようになった ので、項目の値を計算する処理を if 文の後の 8 行目に移動する
  • 今回の記事で先程デバッグのために 9 行目に記述した print(select_values[i]) はもう必要がないので削除した
 1  def create_dropdown(self):
元と同じなので省略
 2      for i in range(2):
 3          # ラベルと項目の値を計算する
 4          if self.mb.ai[i] is None:
 5              label = "人間"
 6          else:
 7              label = self.mb.ai[i].__name__
 8          value = ( self.mb.ai[i], self.params[i] )
元と同じなので省略
 9
10  Marubatsu_GUI.create_dropdown = create_dropdown
行番号のないプログラム
def create_dropdown(self):
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 〇 と × の Dropdown を格納する list
    self.dropdown_list = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
        else:
            label = self.mb.ai[i].__name__        
        value = ( self.mb.ai[i], self.params[i] )
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value
    
        # Dropdown の description を計算する
        description = "" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )

Marubatsu_GUI.create_dropdown = create_dropdown
修正箇所
def create_dropdown(self):
元と同じなので省略
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
-           value = ( None, {} )
        else:
            label = self.mb.ai[i].__name__        
-           value = ( self.mb.ai[i], self.params[i] )
+       value = ( self.mb.ai[i], self.params[i] )
元と同じなので省略
-       print(select_values[i])   

Marubatsu_GUI.create_dropdown = create_dropdown

create_event_handler の修正

下記は create_event_handler の修正です。

  • 5 行目:先ほど説明したように、if 文を使わずに、直接 aiparams に Dropdown の value 属性に代入されている tuple の各要素の値を代入するように修正する
1  def create_event_handler(self):
元と同じなので省略
2      # 変更ボタンのイベントハンドラを定義する
3      def on_change_button_clicked(b):
4          for i in range(2):
5              self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
6          self.mb.play_loop(self, self.params)
元と同じなので省略
7     
8  Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    # 乱数の種のチェックボックスのイベントハンドラを定義する
    def on_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b=None):
        path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                        initialdir="save")
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                change_step(data["move_count"])
                for i in range(2):
                    value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
                    self.dropdown_list[i].value = value               
                if data["seed"] is not None:                   
                    self.checkbox.value = True
                    self.inttext.value = data["seed"]
                else:
                    self.checkbox.value = False
                    
    def on_save_button_clicked(b=None):
        name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                for i in range(2)]
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{name[0]} VS {name[1]} {timestr}"
        path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                            initialdir="save", initialfile=fname,
                                            defaultextension="mbsav")
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
                
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

マスの上でクリックすることで着手を行う。
下記の GUI で操作を行うことができる。
()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。

乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
開く(-,L)\tファイルから対戦データを読み込む
保存(+,S)\tファイルに対戦データを保存する
?(*,H)\t\tこの操作説明を表示する
手番の担当\tメニューからそれぞれの手番の担当を選択する
\t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
変更\t\tゲームの途中で手番の担当を変更する
リセット\t手番の担当を変更してゲームをリセットする
待った(0)\t1つ前の自分の着手をキャンセルする
<<(↑)\t\t最初の局面に移動する
<(←)\t\t1手前の局面に移動する
>(→)\t\t1手後の局面に移動する
>>(↓)\t\t最後の着手が行われた局面に移動する
スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する

手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    self.help_button.on_click(on_help_button_clicked)
    
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
        self.mb.play_loop(self, self.params)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        self.output.clear_output()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.update_gui()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    # step 手目の局面に移動する
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.update_gui()        

    def on_first_button_clicked(b=None):
        change_step(0)

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

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        if self.mb.move_count != changed["new"]:
            change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            with self.output:
                self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self, self.params)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
            "-": on_load_button_clicked,            
            "l": on_load_button_clicked,            
            "+": on_save_button_clicked,            
            "s": on_save_button_clicked,            
            "*": on_help_button_clicked,            
            "h": on_help_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                event.inaxes = True
                event.xdata = num % 3
                event.ydata = 2 - (num // 3)
                on_mouse_down(event)
            except:
                pass
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)       
    
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
-           self.mb.ai[i], self.params[i] = (None, {}) if self.dropdown_list[i].value[0] == "人間" \
-                                                      else self.dropdown_list[i].value
+           self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
        self.mb.play_loop(self, self.params)
元と同じなので省略
     
Marubatsu_GUI.create_event_handler = create_event_handler

update_gui の修正

下記は update_gui の修正です。なお、元のプログラムでは、if 文を使って対戦カードの文字列を計算していましたが、よく考えると Dropdown に選択されている 項目の名前をそのまま対戦カードに表示すればよい ことに気が付きましたので、そのように修正しました。なお、Dropdown に 選択中の項目の名前 は、label 属性に代入 されています。

  • 2 行目:Dropdown の label 属性を使って対戦カードを表示するように修正する
1  def update_gui(self):
元と同じなので省略
2      ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}", fontsize=20, ha="center")   
元と同じなので省略
3
4  Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
    ax = self.ax
    ai = self.mb.ai
    
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}", fontsize=20, ha="center")   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(0, -0.2, text, fontsize=20)
    
    self.draw_board(ax, self.mb)
    
    self.update_widgets_status() 
    
Marubatsu_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
-   names = []
-   for i in range(2):
-       names.append("人間" if ai[i] is None else ai[i].__name__)
-   ax.text(1.5, 3.5, f"{names[0]} VS {names[1]}", fontsize=20, ha="center")   
+   ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}", fontsize=20, ha="center")   
元と同じなので省略

Marubatsu_GUI.update_gui = update_gui

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、様々な操作を行って問題なく対戦を行うことができることを確認して下さい。

gui_play()

また、下記のプログラムでも確認を行ってください。

gui_play(ai=[ai2s, ai2s])

Dropdown に登録されていない AI の登録の改良

致命的なバグではありませんが、下記のプログラムのようにキーワード引数 aiDropdown に登録されていない ai1ai2 を記述して gui_play を実行すると、実行結果の左図のように 〇 の手番の Dropdownai1 しか登録されない というバグが発生しますす。なお、実行結果の右図のように × の 手番の Dropdown には両方の AI が登録 されます。このバグの原因について少し考えてみて下さい。

from ai import ai1, ai2

gui_play(ai=[ai1, ai2])

実行結果

 

このようなことが起きる原因は、gui_play(ai=[ai1, ai2]) を実行すると、下記の create_dropdown に対して、以下のような手順で処理が行われるからです。

  1. 2 ~ 13 行目の 1 回目の繰り返し処理によって、ai1 に対する処理が行われる
  2. ai1 は self.ai_dict のキーの値に存在しないので、5 行目が実行されて ai_dictai1 に対するキーとキーの値が代入される
  3. 10 ~ 12 行目で ai1 が追加して登録された Dropdown が作成されるが、ai2 に対する処理はまだ行われていない ので、作成した Dropwdown には ai2 の項目は存在しない
  4. 2 ~ 13 行目の 2 回目の繰り返し処理によって、ai2 に対する処理が行われる
  5. ai2self.ai_dict のキーの値に存在しないので、5 行目が実行されて ai_dictai2 に対するキーとキーの値が代入される。その際に ai_dict には ai1 のキーとキーの値は代入済 である
  6. 10 ~ 12 行目で ai1ai2 が追加して登録された Dropdown が作成される
 1  def create_dropdown(self):

 2       for i in range(2):

 3           if value not in self.ai_dict.values():
 4               # 項目を登録する
 5               self.ai_dict[label] = value
 6
 7           # Dropdown の description を計算する
 8           description = "" if i == 0 else "×"
 9           self.dropdown_list.append(
10               widgets.Dropdown(
11                   options=self.ai_dict,

12               )
13           )

従って、この問題は下記のプログラムのように、ai_dictai1ai2 の両方を登録した後で、Dropdown を作成する ように修正することで解決することができます。

  • 7 行目改めて for 文を記述する ことで、2 ~ 5 行目の for 文の処理によって ai_dict に項目を登録する 処理が完了してから2 つの Dropdown を作成する ように修正する
 1  def create_dropdown(self):
元と同じなので省略
 2      for i in range(2):
元と同じなので省略
 3          if value not in self.ai_dict.values():
 4              # 項目を登録する
 5              self.ai_dict[label] = value
 6
 7      for i in range(2):
 8          # Dropdown の description を計算する
 9          description = "" if i == 0 else "×"
10          self.dropdown_list.append(
元と同じなので省略
11
12  Marubatsu_GUI.create_dropdown = create_dropdown
行番号のないプログラム
def create_dropdown(self):
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # 〇 と × の Dropdown を格納する list
    self.dropdown_list = []
    # ai に代入されている内容を ai_dict に追加する
    for i in range(2):
        # ラベルと項目の値を計算する
        if self.mb.ai[i] is None:
            label = "人間"
        else:
            label = self.mb.ai[i].__name__        
        value = ( self.mb.ai[i], self.params[i] )
        # value を select_values に常に登録する
        select_values.append(value)
        # value が ai_values に登録済かどうかを判定する
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value

    for i in range(2):
        # Dropdown の description を計算する
        description = "" if i == 0 else "×"
        self.dropdown_list.append(
            widgets.Dropdown(
                options=self.ai_dict,
                description=description,
                layout=widgets.Layout(width="100px"),
                style={"description_width": "20px"},
                value=select_values[i],
            )
        )

Marubatsu_GUI.create_dropdown = create_dropdown
修正箇所
def create_dropdown(self):
元と同じなので省略
    for i in range(2):
元と同じなので省略
        if value not in self.ai_dict.values():
            # 項目を登録する
            self.ai_dict[label] = value

+   for i in range(2):
        # Dropdown の description を計算する
        description = "" if i == 0 else "×"
        self.dropdown_list.append(
元と同じなので省略

Marubatsu_GUI.create_dropdown = create_dropdown

上記の修正後に下記のプログラムを実行すると、実行結果のように 〇 の手番の Dropdown にも ai2 が登録されるようになったことが確認できます。

gui_play(ai=[ai1, ai2])

実行結果

 

今回の記事のまとめ

今回の記事では、データ構造を修正した結果生じた Marubatsu_GUI クラスのバグを修正し、いくつかの改良を行いました。なお、紹介したバグの中で、複数のバグが行った処理 によって 偶然正しい処理が行われてしまう というバグは 実際に悩まされることが多いバグです

これでおそらくゲームを遊ぶという観点での Marubatsu_GUI クラスのバグは修正されたのではないかと思いますが、100 % バグが存在しないかどうかまではわからないので、何かバグが見つかればその都度修正しようと思います。また、何らかのバグを発見した方はコメントで指摘して頂ければ助かります。

なお、ゲームを遊ぶという観点以外では、ゲームの保存と読み込みに関するバグが残っているので、次回の記事で修正することにします。余裕がある方はどのようなバグがあるかについて調べてみて下さい。

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

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
marubatsu_new.py 今回の記事で更新した marubatsu.py
util_new.py 今回の記事で更新した util.py

次回の記事

  1. バグが 3 つ以上存在する場合も同様です

  2. 正しい長方形の面積の式は「縦 × 横」です

  3. 正しい直方体の体積の式は「底面の面積 × 高さ」です

0
1
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?