目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
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_clicked
も on_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_values
に value
の値が追加されます。
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_play
で ai_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_play
は ai2s
のように、ルールベースの 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 行目が実行されてlabel
とvalue
に"人間"
という文字列が代入される - 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 つの間違いを重ねた結果、偶然に正しく 直方体の体積が 計算されてしまいます。
途中の計算が間違っていても、正しい答えが得られるのだから結果オーライだと思う人がいるかもしれませんが、例えば同じプログラムの中で、上記の間違った長方形の面積の計算方法を使って、直方体の表面積を計算すると 間違った答えが計算されてしまいます。このようなバグを放置すると、その場はうまく行くように見えるかもしれませんが、後で予期しない別のバグが発生する原因になる ので、決して放置するべきではありません。
このように、複数のバグが行った処理 によって 偶然正しい処理が行われてしまう というバグはかなり質の悪いバグで、バグの発見や修正が困難です。今回の記事で示したように 実際に発生する場合がある ので紹介しました。
なお、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 文を使わずに、直接
ai
とparams
に 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 の登録の改良
致命的なバグではありませんが、下記のプログラムのようにキーワード引数 ai
に Dropdown に登録されていない ai1
と ai2
を記述して gui_play
を実行すると、実行結果の左図のように 〇 の手番の Dropdown に ai1
しか登録されない というバグが発生しますす。なお、実行結果の右図のように × の 手番の Dropdown には両方の AI が登録 されます。このバグの原因について少し考えてみて下さい。
from ai import ai1, ai2
gui_play(ai=[ai1, ai2])
実行結果
このようなことが起きる原因は、gui_play(ai=[ai1, ai2])
を実行すると、下記の create_dropdown
に対して、以下のような手順で処理が行われるからです。
- 2 ~ 13 行目の 1 回目の繰り返し処理によって、
ai1
に対する処理が行われる -
ai1
はself.ai_dict
のキーの値に存在しないので、5 行目が実行されてai_dict
にai1
に対するキーとキーの値が代入される - 10 ~ 12 行目で
ai1
が追加して登録された Dropdown が作成されるが、ai2
に対する処理はまだ行われていない ので、作成した Dropwdown にはai2
の項目は存在しない - 2 ~ 13 行目の 2 回目の繰り返し処理によって、
ai2
に対する処理が行われる -
ai2
はself.ai_dict
のキーの値に存在しないので、5 行目が実行されてai_dict
にai2
に対するキーとキーの値が代入される。その際にai_dict
にはai1
のキーとキーの値は代入済 である - 10 ~ 12 行目で
ai1
とai2
が追加して登録された 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_dict
に ai1
と ai2
の両方を登録した後で、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 |
次回の記事