目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
今回の記事の内容と用語の定義
前回の記事では、Dropdown によって、任意の AI が計算した候補手または評価値のゲーム盤への表示できるようにしました。今回の記事では、この機能と ai_gt6
の改良を行うことにします。
用語の定義
説明を短くするために、以後の記事で用いるいくつかの用語を定義することにします。
AI の評価値の表示
「AI が計算した候補手または評価値のゲーム盤への表示」という表記は長いので、以後は「AI の評価値の表示」と記述することにします。「評価値の表示」としか表記しませんが、評価値を計算しない AI の場合は候補手を表示する点に注意して下さい。
評価値の Dropdown と 手番の Dropdown
現状では、Marubatsu_GUI クラスは上にある 評価値の表示を行う AI を選択 する Dropdown と、下に 2 つある 手番を担当する AI を選択 する Dropdown を作成します。以後は、前者の Dropdown を「評価値の Dropdown」、後者の Dropdwon を「手番の Dropdown」と表記することにします。
バグの修正
今回の話を始める前に、util.py にバグ がある事が判明したので修正します。
具体的には、以前の記事で gui_play
の下記の 4、5 行目のプログラムを追加して、手番の Dropdown に ai_gtsv
の項目を追加 したのですが、その 次の回からの util.py にその内容が反映されていない ことが判明しました。
1 def gui_play(ai=None, params=None, ai_dict=None, seed=None):
略
2 bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
3 ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
4 bestmoves_by_board_sv = load_bestmoves("../data/bestmoves_by_board_shortest_victory.dat")
5 ai_dict["ai_gtsv"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board_sv})
このバグによって、その回以降から前回の記事までで、gui_play()
を実行した際に ai_gtsv
が項目に登録されない というバグが発生しています。ただし、このバグをさかのぼって修正すると、記事の画像と、実際のプログラムの実行が合わなくなる点と、このバグによって記事の趣旨が大きく変わることはないので、過去の記事をさかのぼってプログラムを修正しないことにし、今回の記事の util.py から修正 することにします。
評価値を表示する AI の自動選択
現状では評価値の Dropdown で選択された AI が計算した評価値が表示されますが、AI どうしの対戦結果 を リプレイモードで検証 する際には、それぞれの局面で、その 手番を担当する AI が計算した評価値 が 自動的に表示 されると便利です。
そのような機能を追加するための ユーザーインターフェース(UI) について少し考えてみて下さい。
UI の検討
簡単に思いつく方法としては、ON にする ことで 手番を担当する AI が評価値の計算を行う ようになる Button、Checkbox、Togglebutton などのウィジェットを追加 するという方法があるでしょう。ただし、ウィジェットを追加すると、その ウィジェットを表示するための場所が必要 になります。また、ウィジェットを増やすと UI の見た目が複雑 になり、操作方法がわかりづらくなる という問題も発生します。
他の方法として、評価値の Dropdown の 項目に、現在の手番の AI を利用するという意味を表す 「手番の AI」という項目を追加する という方法が考えられます。こちらの方法であれば、新しいウィジェットが増えない ので、ウィジェットを追加する方法でおきる問題は発生しません。一方、Dropdown は、普段は複数ある項目の中から 1 つの項目しか表示しない ので、「手番の AI」という項目の存在が気づかれなくなる 可能性が高いという欠点があります。その点、Button などのウィジェットは、常に表示されている ので、そのような機能が存在することが常にわかるようになります。
上記の 2 種類の UI の どちらを採用するか は、それぞれの 利点と欠点を考慮して検討する 必要があります。本記事 では、後者の新しいウィジェットを配置する必要がなく、場所をとらないという利点を重視して、Dropdown に項目を追加するという方法を採用 することにします。前者の方法がふさわしいと思った方や、他にももっとふさわしい UI を思いついた人はその UI をぜひ実装してみて下さい。
Marubatsu_GUI クラスの create_dropdown
メソッドの修正
Dropdown に「手番の AI」という項目を追加する際には、Dropdown を作成する際に記述する 項目のデータを表す dict に「手番の AI」の 項目のデータを設定 する必要があります。どのようなデータを設定すればよいかについて少し考えてみて下さい。
「手番の AI」の項目の値の検討
他の項目では、項目の値 に以下のようなデータを設定しています。
-
(AI の関数, AI のパラメーター)
という tuple を設定する - ただし、手番を人間が担当する場合は
(None, None)
という tuple を設定する
「手番の AI」 の場合は、AI の関数を直接設定することはできない ので、人間の項目のように、設定する tuple をどのように設定するかを考える必要があります。
tuple の 0 番の要素 に設定する内容は、他の項目の 0 番の要素の値と区別 できれば、どのような値を設定してもかまいません。そこで本記事では 自動的(auto) に手番の AI を利用するということから、"Auto"
という文字列を設定 することにします。他のデータを設定したい人は自由に変更して下さい。
「手番の AI」の場合は、AI のパラメータの情報を直接設定することはできない ので、tuple の 1 番の要素 には、人間が担当する場合と同様に None
を設定 することにします。なお、tuple の 1 番の要素に設定するデータは 実際のプログラムで利用することはない のでどのような値を設定してもかまいません。
上記から、「手番の AI」の項目の値 には ("Auto", None)
を設定することにします。
間違った項目の設定方法
現状では、評価値の Dropdown と 手番の Dropdown の 項目の内容は同じ なので、create_dropdown
では、self.ai_dict
に Dropdown の項目を計算 し、それをそのまま使って 下記のプログラムの 5、10 行目のように 両者の Dropdown を作成 しています。
1 def create_dropdown(self):
略(ここで self.ai_dict の内容を計算している)
2 for i in range(2):
略
3 self.dropdown_list.append(
4 widgets.Dropdown(
5 options=self.ai_dict,
略
6 )
7 )
8
9 self.status_dropdown = widgets.Dropdown(
10 options=self.ai_dict,
略
11 )
略
評価値の Dropdown に「手番の AI」という項目を追加するために、下記のプログラムのように、create_dropdown
メソッドを修正すればよいと思う人がいるかもしれません。
-
13 行目:手番の Dropdown を作成した後で、
self.ai_dict
の"手番の AI"
というキーの値に("Auto", None)
を追加する -
15 行目:上記の
self.ai_dict
を利用して評価値の Dropdown を作成する。この部分のプログラムには 修正を行っていない
1 from marubatsu import Marubatsu_GUI
2 import ipywidgets as widgets
3 from copy import deepcopy
4
5 def create_dropdown(self):
元と同じなので省略
6 for i in range(2):
元と同じなので省略
7 self.dropdown_list.append(
8 widgets.Dropdown(
9 options=self.ai_dict,
元と同じなので省略
10 )
11 )
12
13 self.ai_dict["手番の AI"] = ("Auto", None)
14 self.status_dropdown = widgets.Dropdown(
15 options=self.ai_dict,
16 layout=widgets.Layout(width="100px"),
17 style={"description_width": "20px"},
18 value=select_values[0],
19 )
20
21 Marubatsu_GUI.create_dropdown = create_dropdown
行番号のないプログラム
from marubatsu import Marubatsu_GUI
import ipywidgets as widgets
from copy import deepcopy
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],
)
)
self.ai_dict["手番の AI"] = ("Auto", None)
self.ai_dict["ai1s"] = None
self.status_dropdown = widgets.Dropdown(
options=self.ai_dict,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
Marubatsu_GUI.create_dropdown = create_dropdown
修正箇所
from marubatsu import Marubatsu_GUI
import ipywidgets as widgets
from copy import deepcopy
def create_dropdown(self):
元と同じなので省略
for i in range(2):
元と同じなので省略
self.dropdown_list.append(
widgets.Dropdown(
options=self.ai_dict,
元と同じなので省略
)
)
+ self.ai_dict["手番の AI"] = ("Auto", None)
self.status_dropdown = widgets.Dropdown(
options=self.ai_dict,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
Marubatsu_GUI.create_dropdown = create_dropdown
プログラムにある程度慣れた人は、手番の Dropdown と、評価値の Dropdown が 同じ self.ai_dict
を共有する ので、上記のように、self.ai_dict
を修正すると、手番の Dropdown にも「手番の AI」という 項目が追加されてしまう と思うかもしれません。
しかし、以前の記事で説明したように、Dropdown は、項目のデータを表す options
属性 の内容を Dropdown の作成後に変更しても項目の内容が変わらない という性質を持つので、上記の修正を行っても、手番の Dropdown の項目は変化しません。
そのことは、下記のプログラムを実行することで確認できます。実行結果の左図のように、評価値の Dropdown には最後に 「手番の AI」という項目が登録 されていますが、手番の Dropdown には 登録されていません。
from util import gui_play
gui_play()
実行結果
問題が発生する状況とその原因
上記のように、意図したとおりに Dropdown が作成されるので、上記のプログラムで問題はないと思う人がいるかもしれませんが、実は 上記の修正には問題があります。それは、ファイルから Dropdown に登録されていない AI が対戦したファイルを読み込む と、手番の Dropdown に「手番の AI」という項目が登録されてしまう という問題です。
下図は、どちらの Dropdown にも登録されていない ai1
VS ai2
の対戦結果をファイル1から読み込んだ場合の手番の Dropdown の図で、「手番の AI」という項目 が、ファイルを読み込んだ際に新しく登録された ai1
と ai2
の項目の上に 存在しています。
これは、ファイルから対戦結果を読み込んだ際に、Dropdown の項目を更新する ために、以下のような処理を行っているからです。なお、下記のプログラムについて忘れた方は、以前の記事を復習して下さい。
-
6 行目:手番の Dropdown の
options
属性をコピーし、options
という変数に代入する -
7 ~ 10 行目:対戦結果のファイルに Dropdown に登録されていない AI が存在した場合に、
options
にその AI の項目を登録する -
11 ~ 13 行目:
options
を手番の Dropdown のoptions
属性に代入することで、下の Dropdown の項目を新しいoptions
の内容に更新する
6 行目 でコピーする 手番の Dropdown の options
属性 は、先程説明したように 評価値の Dropdown の options
属性と 同じ dict を共有 しているので 「手番の AI」という項目が登録されています。そのため、上記の処理を行うと、手番の Dropdown に「手番の AI」という項目が追加 されてしまうことになります。
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 options = self.dropdown_list[0].options.copy()
7 for i in range(2):
8 value = (self.mb.ai[i], self.params[i])
9 if not value in options.values():
10 options[names[i]] = value
11 for i in range(2):
12 self.dropdown_list[i].options = options
13 self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
略
問題の修正
上記の問題は、評価値の Dropdown と 手番の Dropdown が、今回の記事の修正によって 異なる項目を持つ ようになったにも関わらず、両方の Dropdown の options
属性 が 同じ dict を共有 していることが原因です。
このような場合は、下記のプログラムのように、create_dropdown
で 評価値の Dropdown を作成 する際に、手番の Dropdown の項目を表す options
属性の値をコピー したデータに対して、「手番の AI」という項目を追加 する必要があります。そうすることによって、評価値の Dropdown と 手番の Dropdown の options
属性の値が独立する ことになるので、先程のような問題は発生しなくなります。
-
2 行目:
self.ai_dict
をコピーしたデータをself.status_ai_dict
に代入する -
3 行目:コピーした
self.status_ai_dict
に対して「手番の AI」の項目を追加する -
5 行目:コピーした
self.status_ai_dict
を使って手番のの Dropdown を作成する
1 def create_dropdown(self):
元と同じなので省略
2 self.status_ai_dict = self.ai_dict.copy()
3 self.status_ai_dict["手番の AI"] = ("Auto", None)
4 self.status_dropdown = widgets.Dropdown(
5 options=self.status_ai_dict,
6 layout=widgets.Layout(width="100px"),
7 style={"description_width": "20px"},
8 value=select_values[0],
9 )
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],
)
)
self.status_ai_dict = self.ai_dict.copy()
self.status_ai_dict["手番の AI"] = ("Auto", None)
self.status_dropdown = widgets.Dropdown(
options=self.status_ai_dict,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
Marubatsu_GUI.create_dropdown = create_dropdown
修正箇所
def create_dropdown(self):
元と同じなので省略
+ self.status_ai_dict = self.ai_dict.copy()
- self.ai_dict["手番の AI"] = ("Auto", None)
+ self.status_ai_dict["手番の AI"] = ("Auto", None)
self.status_dropdown = widgets.Dropdown(
- options=self.ai_dict,
+ options=self.status_ai_dict,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[0],
)
Marubatsu_GUI.create_dropdown = create_dropdown
一度作成した Dropdown の項目を後から変更しない場合は、上記の修正を行わなくても問題は発生しません。ただし、異なる項目を持つ複数の Dropdown の options
属性が同じ dict を共有しても問題がないというのは、ipywidgets の Dropdown がたまたまそのように設計されているからであり、一般的ではないと思います。
また、最初は Dropdown の項目を後から変更しないと思っていても、後から変更するような修正を行うことがあるかもしれないので、異なる項目を持つ Dropdown を作成する場合は、options
属性に同じ dict を共有すべきではないでしょう。
上記の修正後に、下記のプログラムを実行して ai1 VS ai2 のファイルを読み込むと、実行結果の左図のように、手番の Dropdown に「手番の AI」という項目が登録されなくなります。一方で、右図のように、評価値の Dropdown に ai1
と ai2
が登録されなくなる という問題が発生します。
gui_play()
実行結果
Marubatsu_GUI クラスの create_event_handler
の修正
上記の問題は、評価値と手番の Dropdown の options
属性が異なるデータになったことが原因 なので、下記のプログラムのように on_load_button_clicked
内で、評価値の Dropdown の項目を更新する処理 を記述する必要があります。下記の修正で行っている処理は、create_dropdown
と同様 に、手番の Dropdown の項目を表す options
属性をコピー し、そのデータに「AI の手番」の項目を追加する というものです。
なお、今回の修正とは別に、13 行目の mb.ai[i]
が間違っていた 点に気が付きましたので、self.mb.ai[i]
のように修正しました。
-
17 行目:手番の Dropdown の項目のデータを表す
options
をコピーしてstatus_options
に代入する -
18、19 行目:
status_options
に「AI の手番」の項目を追加し、評価値の Dropdown のoptions
属性にstatus_options
の値を代入して更新する
1 import math
2 import pickle
3 from tkinter import Tk, filedialog
4
5 def create_event_handler(self):
元と同じなので省略
6 # 開く、保存ボタンのイベントハンドラを定義する
7 def on_load_button_clicked(b=None):
元と同じなので省略
8 if path != "":
9 with open(path, "rb") as f:
元と同じなので省略
10 if "names" in data:
11 names = data["names"]
12 else:
13 names = [ "人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__ for i in range(2)]
元と同じなので省略
14 for i in range(2):
15 self.dropdown_list[i].options = options
16 self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
17 status_options = options.copy()
18 status_options["手番の AI"] = ("Auto", None)
19 self.status_dropdown.options = status_options
元と同じなので省略
20
21 Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
import math
import pickle
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"] if "params" in data else [ {}, {} ]
if "names" in data:
names = data["names"]
else:
names = [ "人間" if self.mb.ai[i] is None else self.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])
status_options = options.copy()
status_options["手番の AI"] = ("Auto", None)
self.status_dropdown.options = status_options
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_show_tree_button_clicked(b=None):
self.show_subtree = not self.show_subtree
self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
self.update_gui()
def on_reset_tree_button_clicked(b=None):
self.update_gui()
def on_help_button_clicked(b=None):
self.help.layout.display = "none" if self.help.layout.display is None else None
self.load_button.on_click(on_load_button_clicked)
self.save_button.on_click(on_save_button_clicked)
self.show_tree_button.on_click(on_show_tree_button_clicked)
self.reset_tree_button.on_click(on_reset_tree_button_clicked)
self.help_button.on_click(on_help_button_clicked)
def on_show_status_button_clicked(b=None):
self.show_status = not self.show_status
self.update_gui()
def on_status_dropdown_changed(changed):
self.update_gui()
def on_size_slider_changed(changed):
self.size = changed["new"]
self.fig.set_figwidth(self.size)
self.fig.set_figheight(self.size)
self.update_gui()
self.show_status_button.on_click(on_show_status_button_clicked)
self.status_dropdown.observe(on_status_dropdown_changed, names="value")
self.size_slider.observe(on_size_slider_changed, names="value")
# 変更ボタンのイベントハンドラを定義する
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
修正箇所
import math
import pickle
from tkinter import Tk, filedialog
def create_event_handler(self):
元と同じなので省略
# 開く、保存ボタンのイベントハンドラを定義する
def on_load_button_clicked(b=None):
元と同じなので省略
if path != "":
with open(path, "rb") as f:
元と同じなので省略
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)]
+ names = [ "人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__ for i in range(2)]
元と同じなので省略
for i in range(2):
self.dropdown_list[i].options = options
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
+ status_options = options.copy()
+ status_options["手番の AI"] = ("Auto", None)
+ self.status_dropdown.options = status_options
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
上記の修正後に、下記のプログラムを実行して ai1 VS ai2 のファイルを読み込むと、実行結果のように、評価値の Dropdown に ai1
と ai2
が登録される ことが確認できます。
gui_play()
実行結果
Marubatsu_GUI クラスの update_gui
メソッドの修正
次に 「手番の AI」が選択されていた場合の処理 を下記のプログラムのように update_gui
に記述 する必要があります。手番の AI の情報 は Marubatsu クラスのインスタンスの ai
属性 に、AI のパラメータの情報 は Marubatsu_GUI クラスの params
属性 に代入されているので、それらを利用します。
なお、ゲーム盤のマスに評価値の値の表示が収まらな_場合がある ことがわかったので、17 行目で 評価値のフォントサイズ を 5 から 4.5 に 少し小さくしました。
-
7 ~ 10 行目:
ai
の値が"Auto"
の場合は、8 行目で現在の手番を表すインデックスを計算し、9、10 行目で手番の AI と パラメーターを計算してai
とparams
に代入する
1 from marubatsu import Marubatsu
2
3 def update_gui(self):
元と同じなので省略
4 if self.show_status:
5 bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
6 ai, params = self.status_dropdown.value
7 if ai == "Auto":
8 index = 0 if self.mb.turn == Marubatsu.CIRCLE else 1
9 ai = self.mb.ai[index]
10 params = self.params[index]
11 if ai is not None:
12 analyze = ai(self.mb, analyze=True, **params)
13 score_by_move = analyze["score_by_move"]
14 candidate = analyze["candidate"]
元と同じなので省略
15 if score_by_move is not None:
16 color = "red" if move in candidate else "black"
17 ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=4.5*self.size, c=color)
元と同じなので省略
18
19 Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
from marubatsu import Marubatsu
def update_gui(self):
def calc_status_txt(score):
if score > 0:
return "〇"
elif score == 0:
return "△"
else:
return "×"
ax = self.ax
# 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=7*self.size, ha="center")
# ゲームの決着がついていない場合は、手番を表示する
if self.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
score = self.score_table[self.mb.board_to_str()]["score"]
if self.show_status:
text += " 状況 " + calc_status_txt(score)
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
# リプレイ中の場合は "Replay" を表示する
if is_replay:
text += " Replay"
ax.text(1.5, -0.2, text, fontsize=7*self.size, ha="center")
self.draw_board(ax, self.mb, lw=0.7*self.size)
if self.show_status:
bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
ai, params = self.status_dropdown.value
if ai == "Auto":
index = 0 if self.mb.turn == Marubatsu.CIRCLE else 1
ai = self.mb.ai[index]
params = self.params[index]
if ai is not None:
analyze = ai(self.mb, analyze=True, **params)
score_by_move = analyze["score_by_move"]
candidate = analyze["candidate"]
for move in self.mb.calc_legal_moves():
x, y = move
mb = deepcopy(self.mb)
mb.move(x, y)
score = self.score_table[mb.board_to_str()]["score"]
color = "red" if move in bestmoves else "black"
text = calc_status_txt(score)
ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
if ai is not None:
if score_by_move is not None:
color = "red" if move in candidate else "black"
ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
elif move in candidate:
ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
self.update_widgets_status()
if hasattr(self, "mbtree_gui"):
from tree import Node
self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
self.mbtree_gui.update_gui()
Marubatsu_GUI.update_gui = update_gui
修正箇所
from marubatsu import Marubatsu
def update_gui(self):
元と同じなので省略
if self.show_status:
bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
ai, params = self.status_dropdown.value
+ if ai == "Auto":
+ index = 0 if self.mb.turn == Marubatsu.CIRCLE else 1
+ ai = self.mb.ai[index]
+ params = self.params[index]
if ai is not None:
analyze = ai(self.mb, analyze=True, **params)
score_by_move = analyze["score_by_move"]
candidate = analyze["candidate"]
元と同じなので省略
if score_by_move is not None:
color = "red" if move in candidate else "black"
- ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
+ ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=4.5*self.size, c=color)
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
上記の修正後に、下記のプログラムを実行して下記の処理を行うことで、正しく処理が行われるかどうかを確認します。
-
ai1s
VSai14s
で対戦を行う - 評価値の Dropdown に「手番の AI」を選択し、「状況」ボタンをクリックする
- リプレイボタンをクリックしてゲーム開始時の局面と 1 手目の局面を表示する
実行結果の左図のように、ゲーム開始時の局面 では ai1s
が計算した評価値 が、右図の 1 手目の局面 では、ai14s
が計算した評価値が表示 されることが確認できます。
gui_play()
実行結果
ゲーム木を利用した AI の評価値の表示
現状では、ゲーム木を利用 して着手を選択する AI である ai_gt6
は、下図のように 候補手を表示 します。
これは、ai_gt6
の仮引数 bestmoves_by_board
には、評価値の情報が記録されていない ためです。しかし、実際には bestmoves_by_board
の元となった、ゲーム木の中には評価値が計算済 なので、ゲーム木を利用した AI も 評価値を表示することが可能 です。
そのために必要となる、ゲーム木の局面と最善手・評価値の対応表 のデータは、bestmoves_and_score_by_board.dat という ファイルに保存済 なので、それを利用して analyze=True
を実引数に記述して呼び出した際に、それぞれの合法手を着手した際の評価値を計算 して返す ai_gt7
という AI の関数を定義する事にします。どのように定義すればよいかについて少し考えてみて下さい。
ai_gt7
の定義の方法
ai_gt7
は、ゲーム木の局面と最善手・評価値の対応表のデータを使って、局面の情報から直接最善手の一覧を計算 する処理を行います。そのため、局面の情報から評価値を計算して返す関数に対して利用できる ai_by_score
のデコレーターを 利用することはできません。
ai_by_candidate
のデコレーターは、下記のプログラムの 5 行目のように、評価値の一覧のデータを返すように作られてい ないため、ai_by_candidate
を 利用することもできません。
1 def ai_by_candidate(func):
略
2 if analyze:
3 return {
4 "candidate": candidate,
5 "score_by_move": None
6 }
略
ai_gt7
と ai_by_candidate
修正する方法
一つの方法として、以下のように ai_gt7
を定義 し、ai_by_candidate
を修正する という方法が考えられます。
-
ai_gt7
が 返り値 として、候補手の一覧 と、それぞれの合法手に着手を行った際の局面の 評価値の一覧 を返すように定義する -
ai_by_candidate
をai_gt7
の返り値を利用して、評価値の一覧を返すように修正 する
しかし、この方法には大きな欠点があります。それは、上記の方法では ai_gt7
が 必ず評価値の一覧を計算する必要 がある点です。
ai_gt7
が 着手を選択する際 に、それぞれの合法手に着手を行った際の局面の 評価値の情報は必要ありません。それにも関わらず、必ずその情報を計算する ように ai_gt7
を定義してしまうと、ai_gt7
の処理が 必要のない処理を行う分だけ遅くなってしまいます。
そのため、本記事ではこの方法は採用しません。
デコレーターを利用しない方法
もう一つの方法は、デコレーターを利用せず に ai_gt7
が ラッパー関数によって追加する処理を含めた 処理を行うようにする方法です。この方法であれば、仮引数 analyze
に True
が代入されている場合のみ、評価値の一覧を計算することができるようになります。
下記は、そのように ai_gt7
を定義したプログラムです。
-
5 行目:仮引数に着手を選択するために必要な
mborig
、bestmoves_and_score_by_board
と、ラッパー関数に記述していたdebug
、rand
、analyze
を全て持つように関数を定義する。局面のデータはコピーする場合があるので、仮引数の名前をmborig
とした -
6 行目:候補手の一覧を
bestmoves_and_score_by_board
の"bestmoves"
属性から取り出して、candidate
に代入する - 7 行目:ラッパー関数で行っていた、候補手のデバッグ表示を行う
-
8 ~ 20 行目:
analyze
がTrue
の場合は、それぞれの合法手に着手を行った場合の評価値を計算する - 9 行目:合法手の一覧を計算する
-
10 行目:それぞれの合法手に着手を行った場合の局面の評価値を記録する
score_by_move
を空の dict で初期化する - 11 ~ 15 行目:それぞれの合法手に対する繰り返し処理を行う
-
12 ~ 14 行目:現在の局面を表す
mborig
をコピーしてmb
に代入し、そのmb
に対して合法手の着手を行う -
15 行目:
mb
の局面の評価値をbestmoves_and_score_by_board
の"score"
属性から取り出して、score_by_move
に記録する - 16 行目:評価値の一覧のデバッグ表示を行う
- 17 ~ 20 行目:候補手の一覧と評価値の一覧を表す dict を返す
-
21 ~ 25 行目:
analyze
がFalse
の場合の処理を行う。この部分はai_by_candidate
のラッパー関数で行う処理と全く同じである
1 from ai import dprint
2 from copy import deepcopy
3 from random import choice
4
5 def ai_gt7(mborig, debug=False, bestmoves_and_score_by_board=None, rand=True, analyze=False):
6 candidate = bestmoves_and_score_by_board[mborig.board_to_str()]["bestmoves"]
7 dprint(debug, "candidate", candidate)
8 if analyze:
9 legal_moves = mborig.calc_legal_moves()
10 score_by_move = {}
11 for move in legal_moves:
12 mb = deepcopy(mborig)
13 x, y = move
14 mb.move(x, y)
15 score_by_move[move] = bestmoves_and_score_by_board[mb.board_to_str()]["score"]
16 dprint(debug, "score_by_move", score_by_move)
17 return {
18 "candidate": candidate,
19 "score_by_move": score_by_move
20 }
21 else:
22 if rand:
23 return choice(candidate)
24 else:
25 return candidate[0]
行番号のないプログラム
from ai import dprint
from copy import deepcopy
from random import choice
def ai_gt7(mborig, debug=False, bestmoves_and_score_by_board=None, rand=True, analyze=False):
candidate = bestmoves_and_score_by_board[mborig.board_to_str()]["bestmoves"]
dprint(debug, "candidate", candidate)
if analyze:
legal_moves = mborig.calc_legal_moves()
score_by_move = {}
for move in legal_moves:
mb = deepcopy(mborig)
x, y = move
mb.move(x, y)
score_by_move[move] = bestmoves_and_score_by_board[mb.board_to_str()]["score"]
dprint(debug, "score_by_move", score_by_move)
return {
"candidate": candidate,
"score_by_move": score_by_move
}
else:
if rand:
return choice(candidate)
else:
return candidate[0]
上記の実行後に、下記のプログラムを実行すると、実行結果のように着手が選択されることが確認できます。なお、ゲーム開始時の局面では、すべての合法手が最善手なので、表示される結果にはすべてのマスが表示される可能性があります。
from util import load_bestmoves
mb = Marubatsu()
bestmoves_and_score_by_board = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
print(ai_gt7(mb, bestmoves_and_score_by_board=bestmoves_and_score_by_board))
実行結果(実行結果はランダムなので下記と異なる場合があります)
(0, 0)
また、下記のプログラムのように、実引数に analyze=True
を記述して ai_gt7
を呼び出すと、実行結果のように候補手の一覧と評価値の一覧が表示されることが確認できます。
from pprint import pprint
pprint(ai_gt7(mb, bestmoves_and_score_by_board=bestmoves_and_score_by_board, analyze=True))
実行結果
{'candidate': [(0, 0),
(1, 0),
(2, 0),
(0, 1),
(1, 1),
(2, 1),
(0, 2),
(1, 2),
(2, 2)],
'score_by_move': {(0, 0): 0,
(0, 1): 0,
(0, 2): 0,
(1, 0): 0,
(1, 1): 0,
(1, 2): 0,
(2, 0): 0,
(2, 1): 0,
(2, 2): 0}}
gui_play
の修正
次に、下記のプログラムのように gui_play
の AI の項目に ai_gt7
を利用した AI を登録 するように修正します。
- 4 行目:bestmoves_and_score_by_board.dat を読み込むように修正する
-
5 行目:項目の名前を
ai_gt7
に修正し、4 行目のデータをパラメータとして利用するように修正する - 6 行目:bestmoves_and_score_by_board_shortest_victory.dat を読み込むように修正する
-
7 行目:6 行目のデータをパラメータとして利用するように修正する。項目の名前は
ai_gtsv
のまま変更しない
1 import ai as ai_module
2
3 def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
4 bestmoves_and_score_by_board = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
5 ai_dict["ai_gt7"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board})
6 bestmoves_and_score_by_board_sv = load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat")
7 ai_dict["ai_gtsv"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board_sv})
元と同じなので省略
行番号のないプログラム
import ai as ai_module
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_and_score_by_board = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
ai_dict["ai_gt7"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board})
bestmoves_and_score_by_board_sv = load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat")
ai_dict["ai_gtsv"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board_sv})
mb = Marubatsu()
mb.play(ai=ai, params=params, ai_dict=ai_dict, seed=seed, gui=True)
修正箇所
import ai as ai_module
def gui_play(ai=None, params=None, ai_dict=None, seed=None):
元と同じなので省略
- bestmoves_by_board = load_bestmoves("../data/bestmoves_by_board.dat")
+ bestmoves_and_score_by_board = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
- ai_dict["ai_gt6"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board})
+ ai_dict["ai_gt7"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board})
- bestmoves_by_board_sv = load_bestmoves("../data/bestmoves__by_board_shortest_victory.dat")
+ bestmoves_and_score_by_board_sv = load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat")
- ai_dict["ai_gtsv"] = (ai_gt6, {"bestmoves_by_board": bestmoves_by_board_sv})
+ ai_dict["ai_gtsv"] = (ai_gt7, {"bestmoves_and_score_by_board": bestmoves_and_score_by_board_sv})
元と同じなので省略
上記の修正後に、下記のプログラムを実行して (1, 1) に着手を行った後で、上の Dropdown に ai_gt7
を選択して「状況」ボタンをクリックすると、実行結果の左図のようにゲーム盤のマスに評価値が表示されるようになったことが確認できます。なお、(1, 1) に着手を行ったのは、ゲーム開始時の局面では、ai_gt7
と ai_gtsv
で表示が変化しないからです。
次に ai_gtsv
を選択すると、実行結果の右図のように、左図とは異なる評価値が表示されることが確認できます。
gui_play()
実行結果
本記事では図示しませんが、興味がある方は ai_gt7
と ai_gtsv
のそれぞれについて、ゲーム盤に表示 される評価値と下の ゲーム木に表示 される 評価値が一致する ことを確認してみて下さい。
今回の記事のまとめ
今回の記事では、評価値を表示する AI に手番の AI が自動的に選択する改良と、ゲーム木を利用した AI が評価値の一覧を計算できるようにする改良を行いました。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu.py | 本記事で更新した marubatsu_new.py |
ai.py | 本記事で更新した ai_new.py |
util.py | 本記事で更新した util_new.py |
次回の記事
-
そのファイルは今回の記事の github の save フォルダ内に ai1 VS ai2 2024年09月11日 15時56分34秒.mbsav という名前で保存しておきましたので、興味がある方は試してみて下さい ↩