目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
対戦カードに表示される AI の名前に関する改良
前回の記事の最後で、Marubatsu_GUI クラスにはゲームの保存と読み込みに関するバグが残っていると説明しましたが、先に別の改良を行うことにします。
それは、対戦カードに表示される AI の名前 です。現状ではゲーム盤の下に表示される対戦カードの AI の名前は、下記の update_widgets
のプログラムのように、Dropdown の項目名が表示される ようになっています。
def update_gui(self):
略
# 対戦カードの文字列を計算する
ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}", fontsize=20, ha="center")
略
また、play
メソッドから play(ai=[ai1s, ai2s], gui=True)
のように AI を指定して GUI で対戦を行った場合は、下記の create_dropdown
の 5 ~ 8、13 ~ 15 行目の処理で Dropdown の項目名が以下のように設定されます。
- 人間が担当する場合は
"人間"
という文字列が設定される - AI が担当する場合は AI の関数名が設定される
1 def create_dropdown(self):
略
2 # ai に代入されている内容を ai_dict に追加する
3 for i in range(2):
4 # ラベルと項目の値を計算する
5 if self.mb.ai[i] is None:
6 label = "人間"
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 # value が ai_values に登録済かどうかを判定する
13 if value not in self.ai_dict.values():
14 # 項目を登録する
15 self.ai_dict[label] = value
16
17 for i in range(2):
18 # Dropdown の description を計算する
19 description = "〇" if i == 0 else "×"
20 self.dropdown_list.append(
21 widgets.Dropdown(
22 options=self.ai_dict,
23 description=description,
24 layout=widgets.Layout(width="100px"),
25 style={"description_width": "20px"},
26 value=select_values[i],
27 )
28 )
このように、AI が担当した場合の Dropdown の項目名は AI の関数名が設定されますが、これでは 同じ AI の関数に対して異なるパラメータを設定した場合 の Dropdown の項目名が同じになってしまい、区別が付けられない という問題が発生します。
そこで、対戦カードに表示する AI の名前を自由に設定できる ように改良することにします。どのようにすれば良いかについて少し考えてみて下さい。
なお、gui_play
で対戦を行う場合は、ai_dict
のキーによって Dropdown の項目名が設定されるので、対戦カードに表示される AI の名前を変えるのは簡単です。
仮引数 names
の追加
本記事では、対戦カードに表示する それぞれの AI の名前を要素とする list を play
メソッドの 仮引数 names
に代入 して設定できるようにすることにします。また、names
の要素の値が None
の場合 は、対応する AI の名前は これまでと同じ方法で設定する ようにします。
他の方法としては、例えば仮引数 params
の names
というキーの値で設定するという方法が考えられます。この方法には、play
メソッドなどに新しい仮引数 names
を追加する必要がないという利点があります。一方で、params
は AI の関数が処理を行う際に必要となるパラメーター という意味があるので、その中に AI の関数が処理を行う際に利用しない関数の名前のデータを入れるのは変 という問題点があるので、本記事ではこの方法は採用しません。
play
メソッドの修正
まず、play
メソッドを下記のプログラムのように修正します。
-
3 行目:デフォルト値を
None
とする仮引数names
を追加する -
6 行目:Marubatsu_GUI クラスのインスタンスを作成する際に、キーワード引数
names=names
を追加する
なお、仮引数 names
の値は Marubatsu_GUI クラスで対戦カードを表示する以外の場面では利用しない ので、Marubatsu クラスの属性に代入して記録する必要はありません
1 from marubatsu import Marubatsu, Marubatsu_GUI
2
3 def play(self, ai, ai_dict=None, params=None, names=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
4 # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
5 if gui:
6 mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, seed=seed, size=size)
元と同じなので省略
7
8 Marubatsu.play = play
行番号のないプログラム
from marubatsu import Marubatsu, Marubatsu_GUI
def play(self, ai, ai_dict=None, params=None, names=None, verbose=True, seed=None, gui=False, size=3):
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# 一部の仮引数をインスタンスの属性に代入する
self.ai = ai
self.verbose = verbose
self.gui = gui
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, seed=seed, size=size)
else:
mb_gui = None
self.restart()
return self.play_loop(mb_gui, params=params)
Marubatsu.play = play
修正箇所
from marubatsu import Marubatsu, Marubatsu_GUI
-def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
+def play(self, ai, ai_dict=None, params=None, names=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
- mb_gui = Marubatsu_GUI(self, params=params, ai_dict=ai_dict, seed=seed, size=size)
+ mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, seed=seed, size=size)
元と同じなので省略
Marubatsu.play = play
Marubatsu_GUI クラスの __init__
メソッドの修正
次に Marubatsu_GUI クラスの __init__
メソッドを下記のプログラムのように修正します。なお、Marubatsu_GUI クラスのインスタンスは Marubatsu クラスの play
メソッド内で作成されることが前提として定義されており、5 行目の下にあった params
が None
の場合に行う処理は、play
メソッドの中で既に行っていたことに気が付きましたので削除しました。同様に、仮引数 seed
、size
のデフォルト値は play
メソッドで設定済なので、それらの仮引数を通常の仮引数に修正しました。
-
5 行目:仮引数
names
を追加し、seed
とsize
を通常の仮引数に修正する - 5 行目の下にあった
params
がNone
の場合に行う処理を削除する -
8、9 行目:
names
がNone
の場合は、両方の名前を自動的に設定することを表す[None, None]
を代入する -
10 ~ 15 行目:それぞれの AI の名前を表す
names
の要素がNone
の場合に、名前をcreate_dropdown
の 5 ~ 8 行目で行っていたのと同じ方法で設定する処理を行う -
16 行目:仮引数
names
を同名の属性に代入する
1 import ipywidgets as widgets
2 from tkinter import Tk, filedialog
3 import os
4
5 def __init__(self, mb, params, names, ai_dict, seed, size):
6 if ai_dict is None:
7 ai_dict = {}
8 if names is None:
9 names = [None, None]
10 for i in range(2):
11 if names[i] is None:
12 if mb.ai[i] is None:
13 names[i] = "人間"
14 else:
15 names[i] = mb.ai[i].__name__
元と同じなので省略
16 self.names = names
元と同じなので省略
17
18 Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
import ipywidgets as widgets
from tkinter import Tk, filedialog
import os
def __init__(self, mb, params, names, ai_dict, seed=None, size=3):
if params is None:
params = [{}, {}]
if ai_dict is None:
ai_dict = {}
if names is None:
names = [None, None]
for i in range(2):
if names[i] is None:
if mb.ai[i] is None:
names[i] = "人間"
else:
names[i] = mb.ai[i].__name__
# JupyterLab からファイルダイアログを開く際に必要な前処理
root = Tk()
root.withdraw()
root.call('wm', 'attributes', '.', '-topmost', True)
# save フォルダが存在しない場合は作成する
if not os.path.exists("save"):
os.mkdir("save")
self.mb = mb
self.ai_dict = ai_dict
self.params = params
self.names = names
self.seed = seed
self.size = size
super(Marubatsu_GUI, self).__init__()
Marubatsu_GUI.__init__ = __init__
修正箇所
import ipywidgets as widgets
from tkinter import Tk, filedialog
import os
-def __init__(self, mb, params, ai_dict, seed=None, size=3):
+def __init__(self, mb, params, names, ai_dict, seed, size):
- if params is None:
- params = [{}, {}]
if ai_dict is None:
ai_dict = {}
+ if names is None:
+ names = [None, None]
+ for i in range(2):
+ if names[i] is None:
+ if mb.ai[i] is None:
+ names[i] = "人間"
+ else:
+ names[i] = mb.ai[i].__name__
元と同じなので省略
+ self.names = names
元と同じなので省略
Marubatsu_GUI.__init__ = __init__
create_dropdown
の修正
最後に create_dropdown
を下記のプログラムのように修正します。
- 2 行目の下にあった、AI の名前を計算する処理は
__init__
メソッドで行うことにしたので削除する -
9 行目:対戦カードの名前は
self.names[i]
に代入されているので、キーの名前をそのように修正する
1 def create_dropdown(self):
元と同じなので省略
2 for i in range(2):
3 value = ( self.mb.ai[i], self.params[i] )
4 # value を select_values に常に登録する
5 select_values.append(value)
6 # value が ai_values に登録済かどうかを判定する
7 if value not in self.ai_dict.values():
8 # 項目を登録する
9 self.ai_dict[self.names[i]] = value
元と同じなので省略
10
11 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):
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[self.names[i]] = 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 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
+ self.ai_dict[self.names[i]] = value
元と同じなので省略
Marubatsu_GUI.create_dropdown = create_dropdown
上記の修正後に下記のプログラムでキーワード引数 names
を記述せずに play
メソッドを実行すると、実行結果のようにこれまでと同じ対戦カードが表示されることが確認できます。
from ai import ai2s
mb = Marubatsu()
mb.play(ai=[None, ai2s], gui=True)
実行結果
次に下記のプログラムで キーワード引数 names
を記述 して play
メソッドを実行すると、実行結果のように 要素に "You"
を代入したほうは You が、None
を代入したほうはこれまでと同様に AI の関数の名前 が表示されることが確認できます。
mb.play(ai=[None, ai2s], names=["You", None], gui=True)
実行結果
ファイルの保存と読み込みに関するバグの修正
Marubatsu_GUI クラスの ファイルの保存と読み込みを行う処理 は、Marubatsu_GUI クラスが AI のパラメータを扱うことができるように修正する前に実装した ので、当然ですが 保存したファイルの中に AI のパラメータは含まれません。また、今回の記事で実装した AI の名前のデータも保存されない ので、それらを保存するように修正する必要があります。
on_save_button_clicked
の修正
ファイルを保存する処理は、下記のプログラムのように、create_event_handler
の中で定義された on_save_button_clicked
内で行われています。
1 def create_event_handler(self):
略
2 def on_save_button_clicked(b=None):
3 name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
4 for i in range(2)]
5 timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
6 fname = f"{name[0]} VS {name[1]} {timestr}"
7 path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
8 initialdir="save", initialfile=fname,
9 defaultextension="mbsav")
10 if path != "":
11 with open(path, "wb") as f:
12 data = {
13 "records": self.mb.records,
14 "move_count": self.mb.move_count,
15 "ai": self.mb.ai,
16 "seed": self.inttext.value if self.checkbox.value else None
17 }
18 pickle.dump(data, f)
略
3 行目は、AI の名前を計算する処理ですが、この処理のままでは 今回の記事で新しく実装した 、AI の名前を キーワード引数 names
で設定した場合の 名前が反映されません。そのため、AI の名前を 選択中の Dropdown の項目名を使って計算する ように修正する必要があります。なお、AI の名前として、Marubatsu_GUI
クラスの names
属性の値をそのまま使えば良いのではないかと思う人がいるかもしれませんが、names
属性の値 は 最初に行われた対戦での AI の名前 であり、現在対戦中の AI の名前ではないの で使うことはできません1。
また、12 ~ 17 行目からファイルには以下のデータを保存することがわかります。
- 〇×ゲームの棋譜を表す
records
属性 - 現在の局面の手数をあらわす
move_count
属性 - それぞれの手番の担当する AI の関数を表す
ai
属性 - 乱数の種に関するデータ
上記の中には、AI が必要とするパラメーター や、AI の名前 の データが存在しない ので、それらのデータを保存するように修正する必要があります。
下記は、そのように on_save_button_clicked
を修正したプログラムです。なお、行番号の無いプログラムはかなり長いので、この後でファイルの読み込みの処理を修正した後でまとめて表記することにします。
-
3、5 行目:AI の名前を選択中の Dropdown の項目名を使って計算するように修正する。なお、複数のデータを扱うのに名前が
name
では変なのでnames
に修正した -
12 行目:AI のパラメーターを
params
のキーの値に代入する -
13 行目:AI の名前を
names
のキーの値に代入する
1 def create_event_handler(self):
略
2 def on_save_button_clicked(b=None):
3 names = [ self.dropdown_list[i].label for i in range(2) ]
4 timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
5 fname = f"{names[0]} VS {names[1]} {timestr}"
略
6 if path != "":
7 with open(path, "wb") as f:
8 data = {
9 "records": self.mb.records,
10 "move_count": self.mb.move_count,
11 "ai": self.mb.ai,
12 "params": self.params,
13 "names": names,
14 "seed": self.inttext.value if self.checkbox.value else None
15 }
16 pickle.dump(data, f)
略
修正箇所
def create_event_handler(self):
略
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)]
+ names = [ self.dropdown_list[i].label for i in range(2) ]
timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
- fname = f"{name[0]} VS {name[1]} {timestr}"
+ fname = f"{names[0]} VS {names[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,
+ "params": self.params,
+ "names": names,
"seed": self.inttext.value if self.checkbox.value else None
}
pickle.dump(data, f)
略
on_load_button_clicked
の修正
ファイルの読み込みの処理は、下記のプログラムのように、create_event_handler
の中で定義された on_load_button_clicked
内で行われています。
1 def create_event_handler(self):
略
2 # 開く、保存ボタンのイベントハンドラを定義する
3 def on_load_button_clicked(b=None):
4 path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
5 initialdir="save")
6 if path != "":
7 with open(path, "rb") as f:
8 data = pickle.load(f)
9 self.mb.records = data["records"]
10 self.mb.ai = data["ai"]
11 change_step(data["move_count"])
12 for i in range(2):
13 value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
14 self.dropdown_list[i].value = value
15 if data["seed"] is not None:
16 self.checkbox.value = True
17 self.inttext.value = data["seed"]
18 else:
19 self.checkbox.value = False
略
上記に対して下記の修正を行う必要があります。
-
self.params
にファイルから読み込んだデータを代入する - 14 行目で、選択中の Dropdown の項目の値を表す
value
属性に (AI の関数, AI のパラメーター) の tuple を代入するように修正する必要がある -
前回の記事で人間を担当する際のデータ構造の修正したので、13 行目の処理は必要が無くなり、
(self.mb.ai[i], self.params[i])
という tuple を代入すればよい
下記は、それらの修正を行ったプログラムです。
-
11 行目:AI のパラメーターを
self.params
に代入する -
14 行目:選択中の Dropdown の項目の値を表す
value
属性に代入するデータを (AI の関数, AI のパラメーター) の tuple に修正する
1 def create_event_handler(self):
略
2 # 開く、保存ボタンのイベントハンドラを定義する
3 def on_load_button_clicked(b=None):
4 path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
5 initialdir="save")
6 if path != "":
7 with open(path, "rb") as f:
8 data = pickle.load(f)
9 self.mb.records = data["records"]
10 self.mb.ai = data["ai"]
11 self.params = data["params"]
12 change_step(data["move_count"])
13 for i in range(2):
14 self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
15 if data["seed"] is not None:
16 self.checkbox.value = True
17 self.inttext.value = data["seed"]
18 else:
19 self.checkbox.value = False
略
修正箇所
def create_event_handler(self):
略
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"]
+ self.params = data["params"]
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
+ self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
if data["seed"] is not None:
self.checkbox.value = True
self.inttext.value = data["seed"]
else:
self.checkbox.value = False
略
上記でファイルに保存した AI の名前を利用していない点が気になった人がいるかもしれませんが、AI の名前のデータは、この後で行う修正で利用します。
下記は修正した create_event_handler
のプログラムです。長いので折りたたみました。
修正した create_event_handler
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"]
self.params = data["params"]
change_step(data["move_count"])
for i in range(2):
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
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):
names = [ self.dropdown_list[i].label for i in range(2) ]
timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
fname = f"{names[0]} VS {names[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,
"params": self.params,
"names": names,
"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
実行結果は省略しますが、上記の修正後に、下記のプログラムを実行し、様々な対戦カードで対戦を行ってファイルを保存し、保存したファイルを読み込めることを確認して下さい。
from util import gui_play
gui_play()
対戦カードの表示が更新されないバグの修正
修正した Marubatsu_GUI クラスにはいくつかのバグがあるので修正することにします。
1 つ目のバグは、ファイルを読み込んだ際に対戦カードの表示が更新されない 点です。例えば「人間 VS 人間」の対戦が行われている際に「ai2s VS ai2s」の対戦結果のファイルを読み込むと、データは正しく読み込めますが、対戦カードの表示が「人間 VS 人間」のまま変化しません。ただし、リプレイボタンをクリックして 手番を前後に移動すると対戦カードの表示が正しくなります。実際にそのようになることを試してみて下さい。また、バグの原因を少し考えてみて下さい。
リプレイ機能で前後の手番を表示すると 対戦カードが正しく表示されるようになる ことから、データは正しく読み込めている ことがわかります。そのことから、ファイルから データを読み込んだ際のゲーム盤の表示の更新の処理に何らかの問題がある ことが推測されます。
そこで、下記の on_load_button_clicked
の中でゲーム盤の表示の更新を行う処理を探してみると、直接画面の表示の更新を行う処理が見当たらない ことがわかります。
1 def on_load_button_clicked(b=None):
2 path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
3 initialdir="save")
4 if path != "":
5 with open(path, "rb") as f:
6 data = pickle.load(f)
7 self.mb.records = data["records"]
8 self.mb.ai = data["ai"]
9 self.params = data["params"]
10 change_step(data["move_count"])
11 for i in range(2):
12 self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
13 if data["seed"] is not None:
14 self.checkbox.value = True
15 self.inttext.value = data["seed"]
16 else:
17 self.checkbox.value = False
この中で、棋譜に従って 実引数に記述した手数までの着手を行う 10 行目の change_step
の定義を見ると、下記のプログラムの 4 行目のように、その中で描画を更新する処理が行われている ことがわかります。
1 def change_step(step):
2 self.mb.change_step(step)
3 # 描画を更新する
4 self.update_gui()
上記から、on_load_button_clicked
が行う処理を整理すると、下記ようになります。
- 6 ~ 9 行目でファイルから読み込んだデータを適切な属性に代入する
- 10 行目で棋譜に従って着手を行い、描画を更新する
- 11、12 行目で Dropdown の項目を読み込んだ対戦カードのデータに設定する
対戦カードの表示 は、選択中の Dropdown の項目名を使って行われます。上記の処理では Dropdown の項目の選択を更新する処理 を行う手順 3 より前の手順 2 で描画の更新を行っている ため、対戦カードの表示が更新されません。これがバグの原因です。
従って、このバグは上記の 手順 2と 手順 3 の順番を入れ替える か、手順 3 の後で描画の更新を行う ようにすることで修正できます。下記は手順 2 と 手順 3 を入れ替えるように修正したプログラムで、本記事ではこちらを採用することにします。
1 def create_event_handler(self):
元と同じなので省略
2 # 開く、保存ボタンのイベントハンドラを定義する
3 def on_load_button_clicked(b=None):
元と同じなので省略
4 for i in range(2):
5 self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
6 change_step(data["move_count"])
元と同じなので省略
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"]
self.params = data["params"]
for i in range(2):
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
change_step(data["move_count"])
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):
names = [ self.dropdown_list[i].label for i in range(2) ]
timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
fname = f"{names[0]} VS {names[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,
"params": self.params,
"names": names,
"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_load_button_clicked(b=None):
元と同じなので省略
- change_step(data["move_count"])
for i in range(2):
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
+ change_step(data["move_count"])
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、現在とは異なる対戦カードのファイルを読み込んだ際に、対戦カードの表示が正しく更新されるようになったことを確認して下さい。
gui_play()
このバグは、ファイルの保存と読み込みの機能を最初に実装した際には発生していませんでした。過去のプログラムが正しく動作していたのは、過去の update_gui
では下記のプログラムのように対戦カードを選択中の Dropdown の項目名ではなく、ai
属性を使って表示していたからです。
# 対戦カードの文字列を計算する
names = []
for i in range(2):
names.append("人間" if ai[i] is None else ai[i].__name__)
ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)
Dropdown に登録されていない AI の対戦ファイルを読み込んだ場合のバグの修正
現状の Marubatsu_GUI には、Dropdown に登録されていない AI の対戦ファイルを読み込んだ場合に エラーが発生する という問題があります。
例えば、下記の手順で操作を行うとエラーが発生します。
1. 下記のプログラムを実行して、gui_play()
を実行した際に Dropdown に登録されない ai1
VS ai2
の対戦を行い、結果をファイルに保存する
from ai import ai1, ai2
gui_play(ai=[ai1, ai2])
2. 下記のプログラムを実行して手順 1 で保存したファイルを読み込む
gui_play()
ファイルを読み込んだ際の実行結果
略
Cell In[8], line 19
17 self.params = data["params"]
18 for i in range(2):
---> 19 self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
20 change_step(data["move_count"])
21 if data["seed"] is not None:
略
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\site-packages\ipywidgets\widgets\widget_selection.py:135, in findvalue(array, value, compare)
133 return next(x for x in array if compare(x, value))
134 except StopIteration:
--> 135 raise ValueError('%r not in array'%value)
TypeError: not all arguments converted during string formatting
最後の行に表示されるエラーメッセージの意味は正直な所よくわかりませんが、raise ValueError('%r not in array'%value) から、value
が配列(array)に存在しないことが原因である可能性が高いことが推測されます。また、エラーメッセージをさかのぼると、下記の処理を実行した結果エラーが発生したことが確認できます。
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
この処理は、(self.mb.ai[i], self.params[i])
という tuple を項目の値として持つ Dropdown の項目を選択するという処理ですが、ai1
も ai2
も gui_play()
を実行した際の Dropdown の項目には存在しない ので、このエラーは Dropdwon に存在しない項目を選択しようとした結果表示されたもの であることが推測されます。
実際に on_load_button_clicked
の中では、Dropdown に存在しない AI をファイルから読み込んだ際に、その項目を 登録するという処理を行っていない ので、その処理を記述することでこのエラーを解消できる可能性が高いことが推測されます。
既に作成されている Dropdown への項目の追加の方法
on_load_button_clicked
に記述する必要があるのは、既に作成されている Dropdown に項目を追加する という処理なので全く同じではありませんが、Dropdown を作成する前 に Dropdown の項目の一覧を表すデータに 項目を追加するという処理 であれば、下記の create_dropdown
の 7 ~ 9 行目で行っています。
1 def create_dropdown(self):
略
2 for i in range(2):
3 value = ( self.mb.ai[i], self.params[i] )
4 # value を select_values に常に登録する
5 select_values.append(value)
6 # value が ai_values に登録済かどうかを判定する
7 if value not in self.ai_dict.values():
8 # 項目を登録する
9 self.ai_dict[self.names[i]] = value
略
Dropdown の項目の一覧 に関するデータは、options
属性に代入されている ので、上記のプログラムを参考に、下記のように on_load_button_clicked
を修正すれば良いと思った人が多いかもしれません。
-
9 行目:ファイルから読み込んだ AI の名前を
names
に代入する -
10 ~ 14 行目:それぞれの Dropdown の項目の一覧を表す
options
属性に代入された dict のキーの値に、ファイルから読み込んだ (AI の関数, AI のパラメータ) が存在するかどうかを判定し、存在しない場合は 13、14 行目で、両方の Dropdown にその AI の項目を登録する
1 def create_event_handler(self):
略
2 def on_load_button_clicked(b=None):
略
3 if path != "":
4 with open(path, "rb") as f:
5 data = pickle.load(f)
6 self.mb.records = data["records"]
7 self.mb.ai = data["ai"]
8 self.params = data["params"]
9 names = data["names"]
10 for i in range(2):
11 value = (self.mb.ai[i], self.params[i])
12 if not value in self.dropdown_list[i].options.value():
13 self.dropdown_list[0].options[names[i]] = value
14 self.dropdown_list[1].options[names[i]] = value
15 for i in range(2)
16 self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
17 change_step(data["move_count"])
18 if data["seed"] is not None:
19 self.checkbox.value = True
20 self.inttext.value = data["seed"]
21 else:
22 self.checkbox.value = False
略
修正箇所
def create_event_handler(self):
略
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"]
self.params = data["params"]
+ names = data["names"]
+ for i in range(2):
+ value = (self.mb.ai[i], self.params[i])
+ if not value in self.dropdown_list[i].options.value():
+ self.dropdown_list[0].options[names[i]] = value
+ self.dropdown_list[1].options[names[i]] = value
for i in range(2):
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
change_step(data["move_count"])
if data["seed"] is not None:
self.checkbox.value = True
self.inttext.value = data["seed"]
else:
self.checkbox.value = False
略
ただし、上記の方法では Dropdown の項目を新しく登録することはできませんでした。上記の例ではわかりづらいので、別の例を使って具体的に説明します。
下記のプログラムは、a と b という 2 つの項目を持つ Dropdown を作成して表示するプログラムで、実行結果のように、そのような Dropdown が作成されることが確認できます。
d = widgets.Dropdown(
options = { "a": 1, "b": 2 }
)
display(d)
実行結果
この Dropdown に c という項目を追加しようとして、下記のプログラムのように c のキーの値を代入すると、options
属性には確かに c というキーとその値が追加されますが、上記で作成された Dropdown には実行結果のように c という項目は追加されません。
d.options["c"] = 3
print(d.options)
実行結果
{'a': 1, 'b': 2, 'c': 3}
色々と試した所、作成済の Dropdown の項目を後から更新する ためには、更新後の項目を表す 別のオブジェクトを options
属性に代入しなおす必要がある ことが判明しました2。
具体的には、下記のプログラムのように、options
属性に代入された dict を別のオブジェクトにコピー し、コピーした dict に キーを追加して options
属性に代入し直す と実行結果のように Dropdown に c と d の項目が追加されます。なお、dict には自身の浅いコピーを行った dict を返す copy
メソッドがある ので、copy モジュールをインポートする必要はありません3。また、新しい値を代入する前に options
属性に代入されていたデータは、二度と利用することはないので options
属性に対して深いコピーを行う必要はありません。
options = d.options.copy()
options["d"] = 4
d.options = options
細かい話なので、下記の意味が良くわからない人は無視してもかまいません。
色々試してみた所、厳密には options
属性に「異なるオブジェクト」なおかつ、「異なる内容を持つオブジェクト」を代入する必要があるようです。わかりづらいと思いますので具体例をいくつか挙げます。
例えば、下記のプログラムのように、options
属性をコピーせずにそのまま options
という変数に代入して共有し、c というキーに値を代入した後で options
属性に options
を代入しても、options
属性に代入されたオブジェクトが変化しないので、Dropdown の項目は更新されません。
d = widgets.Dropdown(
options = { "a": 1, "b": 2 }
)
display(d)
options = d.options
options["c"] = 3
d.options = options
また、上記の処理の後で、下記のプログラムのように options
属性をコピーし、それをそのまま options
属性に代入した場合は、Dropdown の項目は a と b のままで、options
には a、b、c の 3 つの項目を表すデータが代入されているので、Dropdown の項目が更新されるのはないかと思う人が多いかもしれません。筆者もそう思っていたのですが、実際には Dropdown の項目は更新されませんでした。
options = d.options.copy()
d.options = options
これは筆者の推測なのですが、options
属性の値が実際の Dropdown の項目と一致しているかどうかに関わらず、options
属性の値と options
属性に代入されたデータの内容が一致している場合は Dropdown の項目は変化しないようです。
先程の例で、options
をコピーした後で d の項目を追加したのはそのためです。
on_load_button_clicked
の修正
上記から、作成済の Dropdown に後から項目を追加するには、options
属性の値をコピーした dict に対して項目のデータを追加して options
属性に代入すればよいことがわかりましたので、on_load_button_clicked
を下記のように修正します。
-
12 行目:ファイルから読み込んだ AI の名前をローカル変数
names
に代入する -
13 行目:Dropdown の項目のデータを表す dict を
copy
メソッドでコピーしてローカル変数options
に代入する。なお、2 つの Dropdown の項目 は どちらも同じ なので、片方だけをコピーすればよい -
14 ~ 17 行目:
options
のキーの値に、ファイルから読み込んだ (AI の関数, AI のパラメータ) が存在するかどうかを判定し、存在しない場合は 17 行目でoptions
の AI の名前のキーの値にそのデータを代入する -
19 行目:それぞれの Dropdown の項目のデータを
options
のデータで更新する。なお、ファイルから読み込んだ AI の項目が Dropdown に登録済の場合は、options
のデータは変化しないので、この処理を行っても Dropdown の項目は変化しない - 20 行目:19 行目の処理によって、20 行目の処理で Dropdown に登録されていない項目が選択されてエラーが発生することが無くなる
1 def create_event_handler(self):
元と同じなので省略
2 # 開く、保存ボタンのイベントハンドラを定義する
3 def on_load_button_clicked(b=None):
4 path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
5 initialdir="save")
6 if path != "":
7 with open(path, "rb") as f:
8 data = pickle.load(f)
9 self.mb.records = data["records"]
10 self.mb.ai = data["ai"]
11 self.params = data["params"]
12 names = data["names"]
13 options = self.dropdown_list[0].options.copy()
14 for i in range(2):
15 value = (self.mb.ai[i], self.params[i])
16 if not value in options.values():
17 options[names[i]] = value
18 for i in range(2):
19 self.dropdown_list[i].options = options
20 self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
21 change_step(data["move_count"])
元と同じなので省略
22
23 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"]
self.params = data["params"]
names = data["names"]
options = self.dropdown_list[0].options.copy()
for i in range(2):
value = (self.mb.ai[i], self.params[i])
if not value in options.values():
options[names[i]] = value
for i in range(2):
self.dropdown_list[i].options = options
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
change_step(data["move_count"])
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):
names = [ self.dropdown_list[i].label for i in range(2) ]
timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
fname = f"{names[0]} VS {names[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,
"params": self.params,
"names": names,
"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_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"]
self.params = data["params"]
+ names = data["names"]
+ options = self.dropdown_list[0].options.copy()
+ for i in range(2):
+ value = (self.mb.ai[i], self.params[i])
+ if not value in options.values():
+ options[names[i]] = value
for i in range(2):
+ self.dropdown_list[i].options = options
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
change_step(data["move_count"])
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
実行結果は省略しますが上記の修正を行った後で下記のプログラムを実行し、ai1
VS ai2
の対戦のデータを読み込んでもエラーが発生しなくなったことを確認して下さい。
gui_play()
互換性の問題の修正
今回の記事で、ファイルに保存するデータの データ構造を変えたため、過去に保存したファイルを読み込むことができなくなる という 互換性の問題 が発生しています。
下記は、過去に保存したファイルを読み込んだ場合に表示されるエラーメッセージです。
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Cell In[16], line 17
15 self.mb.records = data["records"]
16 self.mb.ai = data["ai"]
---> 17 self.params = data["params"]
18 names = data["names"]
19 options = self.dropdown_list[0].options.copy()
KeyError: 'params'
エラーメッセージから読み込んだデータに names
というキーが存在しないことが確認できますが、これは当然の結果でしょう。今回の記事の github の save フォルダに「古いバージョンのセーブファイル.mbsav」というファイルを保存しておくので、興味がある方は実際に試してみて下さい。
過去に保存したデータを読み込むことができるようにするためには、下記のプログラムのように、ファイルから読み込んだ dict の中に "params"
と "names"
が存在しなかった場合の処理を記述する必要があります。
-
4 行目:
data
に"params"
というキーが存在しない場合は、self.params
に[ {}, {} ]
を代入するように修正する -
5 ~ 8 行目:
data
に"names"
というキーが存在しない場合は、names
に以前と同じ方法で計算した名前を表す list を計算して代入するように修正する
1 def create_event_handler(self):
元と同じなので省略
2 # 開く、保存ボタンのイベントハンドラを定義する
3 def on_load_button_clicked(b=None):
元と同じなので省略
4 self.params = data["params"] if "params" in data else [ {}, {} ]
5 if "names" in data:
6 names = data["names"]
7 else:
8 names = [ "人間" if mb.ai[i] is None else mb.ai[i].__name__ for i in range(2)]
元と同じなので省略
9
10 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"]
self.params = data["params"] if "params" in data else [ {}, {} ]
if "names" in data:
names = data["names"]
else:
names = [ "人間" if mb.ai[i] is None else mb.ai[i].__name__ for i in range(2)]
options = self.dropdown_list[0].options.copy()
for i in range(2):
value = (self.mb.ai[i], self.params[i])
if not value in options.values():
options[names[i]] = value
for i in range(2):
self.dropdown_list[i].options = options
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
change_step(data["move_count"])
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):
names = [ self.dropdown_list[i].label for i in range(2) ]
timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
fname = f"{names[0]} VS {names[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,
"params": self.params,
"names": names,
"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_load_button_clicked(b=None):
元と同じなので省略
- self.params = data["params"]
+ self.params = data["params"] if "params" in data else [ {}, {} ]
- names = data["names"]
+ if "names" in data:
+ names = data["names"]
+ else:
+ names = [ "人間" if mb.ai[i] is None else mb.ai[i].__name__ for i in range(2)]
options = self.dropdown_list[0].options.copy()
for i in range(2):
value = (self.mb.ai[i], self.params[i])
if not value in options.values():
options[names[i]] = value
for i in range(2):
self.dropdown_list[i].options = options
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
change_step(data["move_count"])
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
上記の修正後に下記のプログラムを実行し、過去のバージョンのファイルを正しく読み込むことができるようになったことを確認して下さい。
gui_play()
互換性に対する補足
ファイルの互換性はあると便利ですが、ファイルに保存する データ構造を何度も変更 すると、互換性への対応の処理がどんどん増えていく ため、プログラムが非常にわかりづらくなることが良くあります。そのため、互換性の対応の処理が ある程度以上複雑になった時点で、古いバージョンへの互換性を切り捨てる ということが良く行われます。
? ボタンの表示の修正
細かい点ですが、下図のように ? ボタンに .. が表示される点が気になったので修正することにします。
色々と試してみた結果、この .. は ボタンの幅が狭すぎて ボタンの中の 文字列がうまく表示しきれない場合に表示される ことがわかりました。なお、上図では ? がうまく表示されているように見えるかもしれませんが、幅が狭すぎて ? が 中央揃えで表示されていません。
そのため、ボタンの幅を広げることで .. が表示されないようになります。試行錯誤の結果、? ボタンの幅を 33 以上にすれば .. が表示されないようになることが判明しましたので、下記のプログラムの 3 ~ 5 行目のように ? ボタンの幅を 30 から 34 に 4 増やし、代わりに開くと保存ボタンの幅を 2 ずつ減らすことで全体のバランスをとることにしました。
1 def create_widgets(self):
元と同じなので省略
2 # 読み書き、ヘルプのボタンを作成する
3 self.load_button = self.create_button("開く", 78)
4 self.save_button = self.create_button("保存", 78)
5 self.help_button = self.create_button("?", 34)
元と同じなので省略
6
7 Marubatsu_GUI.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
# 乱数の種の Checkbox と IntText を作成する
self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
indent=False, layout=widgets.Layout(width="100px"))
self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
layout=widgets.Layout(width="100px"))
# 読み書き、ヘルプのボタンを作成する
self.load_button = self.create_button("開く", 78)
self.save_button = self.create_button("保存", 78)
self.help_button = self.create_button("?", 34)
# AI を選択する Dropdown を作成する
self.create_dropdown()
# 変更、リセット、待ったボタンを作成する
self.change_button = self.create_button("変更", 50)
self.reset_button = self.create_button("リセット", 80)
self.undo_button = self.create_button("待った", 60)
# リプレイのボタンとスライダーを作成する
self.first_button = self.create_button("<<", 50)
self.prev_button = self.create_button("<", 50)
self.next_button = self.create_button(">", 50)
self.last_button = self.create_button(">>", 50)
self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
# ゲーム盤の画像を表す figure を作成する
self.create_figure()
# print による文字列を表示する Output を作成する
self.output = widgets.Output()
Marubatsu_GUI.create_widgets = create_widgets
修正箇所
def create_widgets(self):
元と同じなので省略
# 読み書き、ヘルプのボタンを作成する
- self.load_button = self.create_button("開く", 80)
+ self.load_button = self.create_button("開く", 78)
- self.save_button = self.create_button("保存", 80)
+ self.save_button = self.create_button("保存", 78)
- self.help_button = self.create_button("?", 30)
+ self.help_button = self.create_button("?", 34)
元と同じなので省略
Marubatsu_GUI.create_widgets = create_widgets
上記の修正後に下記のプログラムを実行すると、実行結果のように ? ボタンに .. が表示されなくなったことが確認できます。
gui_play()
今回の記事のまとめ
今回の記事ではファイルの保存と読み込みのバグの修正と、改良を行いました。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu_new.py | 今回の記事で更新した marubatsu.py |
次回の記事
-
筆者も最初は勘違いして
names
属性を使って実装してしまいました ↩ -
このことに関して ipywidgets のドキュメントを探してみたのですが、どこに記載されているかはよくわかりませんでした。ご存じの方がいればコメントで教えて頂ければ助かります ↩
-
深いコピーは copy モジュールの
deepcopy
を利用する必要があります ↩