目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
Dropdown による最善手・評価値の対応表の変更
前回の記事で、Mbtree_GUI クラスのインスタンスを作成する際に、実引数で局面と最善手・評価値の対応表を選択 できるように修正しました。今回の記事では、Mbtree_GUI クラスのインスタンスを 作成した後で、局面と最善手・評価値の対応表を 変更できるようにします。
具体的には、Marubatsu_GUI クラスで AI の選択を Dropdown で行えるようにしたのと同様に、Dropdown を使って 局面と最善手・評価値の対応表を 変更できるようにします。
Mbtree_GUI クラスの __init__
メソッドの修正
Marubatsu_GUI クラスの __init__
メソッドでは、下記のプログラムのように 仮引数 ai_dict
に Dropdown に登録する AI の一覧を表すデータを dict の形式で代入しました。
def __init__(self, mb, params, names, ai_dict, seed, size):
そこで、Mbtree_GUI クラスの __init__
メソッドも同様に、scoretable_dict
という名前の仮引数に Dropdown に登録する 局面と最善手・評価値の対応表の一覧を表すデータを代入 することにします。なお、仮引数の名前を bestmoves_and_score_by_board_dict
とするのはさすがに長すぎる気がしたので、scoretable_dict
としました。
scoretable_dict
には、Dropdown を作成する際にキーワード引数 options
に代入するデータ を代入します。具体的には、下記のプログラムのような、キー に Dropdown に表示する 項目名 を、キーの値 に 局面と最善手・評価値の対応表のデータ を代入します。
なお、基本となる bestmoves_and_score_by_board.dat に対応する項目名は「標準」を表す Standard としました。また、Dropdown に表示する項目名の末尾には () の中にその対応表を利用する AI の名前を記述しました。
from util import load_bestmoves
scoretable_dict = {
"Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
"Shortest victory (ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
}
下記は、__init__
メソッドを修正したプログラムです。scoretable_dict
に対応するキーワード引数を省略できるように None
をデフォルト値とする仮引数とし、None
が代入されている場合は上記のデータを代入することにします。
-
4 行目:仮引数
bestmoves_and_score_by_board
を削除し、デフォルト値をNone
とする仮引数scoretable_dict
を追加する -
5 ~ 11 行目:
scoretable_dict
がNone
の場合に、上記のデータをscoretable_dict
に代入する -
6 行目:9、10 行目の処理で
load_bestmoves
が必要になるが、mbtree.py の先頭でload_bestmoves
を util.py からインポートすると循環参照が発生するので、ローカルなインポートでload_bestmoves
をインポートする必要がある -
12 行目:
bestmoves_and_score_by_board
を同名の属性に代入する処理を削除し、代わりにscoretable_dict
を同名の属性に代入する処理を記述する
1 from tree import Node, Mbtree_GUI
2 from marubatsu import Marubatsu
3
4 def __init__(self, scoretable_dict=None, show_score=True, size=0.15):
5 if scoretable_dict is None:
6 from util import load_bestmoves
7
8 scoretable_dict = {
9 "Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
10 "Shortest victory (ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
11 }
12 self.scoretable_dict = scoretable_dict
元と同じなので省略
13
14 Mbtree_GUI.__init__ = __init__
行番号のないプログラム
from tree import Node, Mbtree_GUI
from marubatsu import Marubatsu
def __init__(self, scoretable_dict=None, show_score=True, size=0.15):
if scoretable_dict is None:
scoretable_dict = {
"Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
"Shortest victory (ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
}
self.scoretable_dict = scoretable_dict
self.show_score = show_score
self.size = size
self.width = 50
self.height = 65
self.selectednode = Node(Marubatsu())
super(Mbtree_GUI, self).__init__()
Mbtree_GUI.__init__ = __init__
修正箇所
from tree import Node, Mbtree_GUI
from marubatsu import Marubatsu
-def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):
+def __init__(self, scoretable_dict=None, show_score=True, size=0.15):
+ if scoretable_dict is None:
+ scoretable_dict = {
+ "Standard (ai_gt6)": load_bestmoves("../data/bestmoves_and_score_by_board.dat"),
+ "Shortest victory(ai_gtsv)": load_bestmoves("../data/bestmoves_and_score_by_board_shortest_victory.dat"),
+ }
- self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
+ self.scoretable_dict = scoretable_dict
self.scoretable_dict = scoretable_dict
元と同じなので省略
Mbtree_GUI.__init__ = __init__
Mbtree_GUI クラスの create_widgets
メソッドの修正
次に、scoretable_dict
の値を元に、Dropdown のウィジェットを作成 する必要があります。下記は、そのように create_widgets
メソッドを修正したプログラムです。
-
5 ~ 8 行目:
scoretable_dict
属性の値を使って Dropdown を作成する。なお、Dropdown の説明文(description)は score table とした -
9 行目:先程
__init__
メソッド内でbestmoves_and_score_by_board
属性に値を代入する処理を削除したが、局面と最善手・評価値の対応表を表す この属性はupdate_gui
で部分木を描画する際に必要 となる。Dropdown の項目の値には局面と最善手・評価値の対応表のデータが代入されているので、Dropdown で 選択されている項目の値(value 属性)を その属性に代入する
1 import ipywidgets as widgets
2 import matplotlib.pyplot as plt
3
4 def create_widgets(self):
元と同じなので省略
5 self.dropdown = widgets.Dropdown(
6 options=self.scoretable_dict,
7 description="score table",
8 )
9 self.bestmoves_and_score_by_board = self.dropdown.value
10
11 Mbtree_GUI.create_widgets = create_widgets
行番号のないプログラム
import ipywidgets as widgets
import matplotlib.pyplot as plt
def create_widgets(self):
self.output = widgets.Output()
self.print_helpmessage()
self.output.layout.display = "none"
self.left_button = self.create_button("←", 50)
self.up_button = self.create_button("↑", 50)
self.right_button = self.create_button("→", 50)
self.down_button = self.create_button("↓", 50)
self.score_button = self.create_button("評価値の表示", 100)
self.size_slider = widgets.FloatSlider(min=0.05, max=0.25, step=0.01, description="size", value=self.size)
self.help_button = self.create_button("?", 50)
self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
with plt.ioff():
self.fig = plt.figure(figsize=[self.width * self.size,
self.height * self.size])
self.ax = self.fig.add_axes([0, 0, 1, 1])
self.fig.canvas.toolbar_visible = False
self.fig.canvas.header_visible = False
self.fig.canvas.footer_visible = False
self.fig.canvas.resizable = False
self.dropdown = widgets.Dropdown(
options=self.scoretable_dict,
description="score table",
)
self.bestmoves_and_score_by_board = self.dropdown.value
Mbtree_GUI.create_widgets = create_widgets
修正箇所
import ipywidgets as widgets
import matplotlib.pyplot as plt
def create_widgets(self):
元と同じなので省略
+ self.dropdown = widgets.Dropdown(
+ options=self.scoretable_dict,
+ description="score table",
+ )
+ self.bestmoves_and_score_by_board = self.dropdown.value
Mbtree_GUI.create_widgets = create_widgets
Mbtree_GUI クラスの display_widgets
メソッドの修正
次に、作成した Dropdown を表示 するようにする必要があります。本記事では、下記のプログラムの 5 行目のように、部分木の大きさを変更する IntSlider の下に Dropdown を配置して表示 することにしました。
1 def display_widgets(self):
2 hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
3 hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
4 self.size_slider, self.help_button])
5 hbox3 = widgets.HBox([self.label, self.down_button, self.label, self.dropdown])
6 self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
7 display(self.vbox)
8
9 Mbtree_GUI.display_widgets = display_widgets
行番号のないプログラム
def display_widgets(self):
hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
self.size_slider, self.help_button])
hbox3 = widgets.HBox([self.label, self.down_button, self.label, self.dropdown])
self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
display(self.vbox)
Mbtree_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):
hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
self.size_slider, self.help_button])
- hbox3 = widgets.HBox([self.label, self.down_button, self.label])
+ hbox3 = widgets.HBox([self.label, self.down_button, self.label, self.dropdown])
self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
display(self.vbox)
Mbtree_GUI.display_widgets = display_widgets
上記の修正後に下記のプログラムを実行すると、実行結果のように 2 つの項目が登録された Dropdown が表示されるようになります。なお、実引数 scoretable_dict
を省略できる ように __init__
メソッドを定義したので、以後は省略することにします。
Mbtree_GUI(scoretable_dict)
実行結果
Mbtree_GUI クラスの create_event_handler
メソッドの修正
次に、Dropdown の項目を変更した際 に、部分木を表示する際に利用する 局面と最善手・評価値の対応表が変わる ようにする必要があります。そのためには、下記のプログラムのように、Dropdown の項目が変更された場合の イベントハンドラ を create_event_handler
の中に 記述する必要 があります。
- 4 ~ 6 行目:Dropdown を変更した際に実行するイベントハンドラを定義する
-
5 行目:
bestmoves_and_score_by_board
属性の値を、Dropdown で選択中の項目の値で更新する(この部分は、changed["new"]
を代入しても良い) -
6 行目:
update_gui
メソッドを呼び出して、部分木の表示を更新する - 8 行目:Dropdown とイベントハンドラを結び付ける
1 def create_event_handler(self):
元と同じなので省略
2 self.help_button.on_click(on_help_button_clicked)
3
4 def on_dropdown_changed(changed):
5 self.bestmoves_and_score_by_board = self.dropdown.value
6 self.update_gui()
7
8 self.dropdown.observe(on_dropdown_changed, names="value")
9
10 def on_key_press(event):
元と同じなので省略
11
12 Mbtree_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
def on_left_button_clicked(b=None):
if self.selectednode.parent is not None:
self.selectednode = self.selectednode.parent
self.update_gui()
def on_right_button_clicked(b=None):
if len(self.selectednode.children) > 0:
self.selectednode = self.selectednode.children[0]
self.update_gui()
def on_up_button_clicked(b=None):
if self.selectednode.parent is not None:
index = self.selectednode.parent.children.index(self.selectednode)
if index > 0:
self.selectednode = self.selectednode.parent.children[index - 1]
self.update_gui()
def on_down_button_clicked(b=None):
if self.selectednode.parent is not None:
index = self.selectednode.parent.children.index(self.selectednode)
if self.selectednode.parent.children[-1] is not self.selectednode:
self.selectednode = self.selectednode.parent.children[index + 1]
self.update_gui()
def on_score_button_clicked(b=None):
self.show_score = not self.show_score
self.update_gui()
def on_size_slider_changed(changed):
self.size = changed["new"]
self.fig.set_figwidth(self.width * self.size)
self.fig.set_figheight(self.height * self.size)
self.update_gui()
def on_help_button_clicked(b=None):
self.output.layout.display = "none" if self.output.layout.display is None else None
self.update_gui()
self.left_button.on_click(on_left_button_clicked)
self.right_button.on_click(on_right_button_clicked)
self.up_button.on_click(on_up_button_clicked)
self.down_button.on_click(on_down_button_clicked)
self.score_button.on_click(on_score_button_clicked)
self.size_slider.observe(on_size_slider_changed, names="value")
self.help_button.on_click(on_help_button_clicked)
def on_dropdown_changed(changed):
self.bestmoves_and_score_by_board = self.dropdown.value
self.update_gui()
self.dropdown.observe(on_dropdown_changed, names="value")
def on_key_press(event):
keymap = {
"left": on_left_button_clicked,
"0": on_left_button_clicked,
"right": on_right_button_clicked,
"up": on_up_button_clicked,
"down": on_down_button_clicked,
}
if event.key in keymap:
keymap[event.key]()
else:
try:
num = int(event.key) - 1
x = num % 3
y = 2 - (num // 3)
move = (x, y)
if move in self.selectednode.children_by_move:
self.selectednode = self.selectednode.children_by_move[move]
self.update_gui()
except:
pass
def on_mouse_down(event):
for rect, node in self.mbtree.nodes_by_rect.items():
if rect.is_inside(event.xdata, event.ydata):
self.selectednode = node
self.update_gui()
break
# fig の画像イベントハンドラを結び付ける
self.fig.canvas.mpl_connect("key_press_event", on_key_press)
self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)
Mbtree_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
self.help_button.on_click(on_help_button_clicked)
+ def on_dropdown_changed(changed):
+ self.bestmoves_and_score_by_board = self.dropdown.value
+ self.update_gui()
+ self.dropdown.observe(on_dropdown_changed, names="value")
def on_key_press(event):
元と同じなので省略
Mbtree_GUI.create_event_handler = create_event_handler
上記の修正後に下記のプログラムを実行し、(0, 0) に着手を行った局面を選択した後で Dropdown で項目を変更すると、実行結果の左図のように部分木の評価値の表示が変更されます。Dropdown の項目を変更する前の右図と見比べて下さい。
Mbtree_GUI()
実行結果
これで、Mbtree_GUI クラスのインスタンスを作成した後で、局面と最善手・評価値の対応表をいつでも変更できるようになりました。
Marubatsu クラスの修正
Marubatsu_GUI クラスを修正した結果、下記のプログラムで gui_play
で対戦を行おうとすると実行結果のような エラーが発生 するようになります。
from util import gui_play
gui_play()
実行結果
略
File c:\Users\ys\ai\marubatsu\123\marubatsu.py:604, in Marubatsu_GUI.__init__(self, mb, params, names, ai_dict, seed, size)
602 if Marubatsu_GUI.mbtree is None:
603 Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
--> 604 self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
略
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\site-packages\ipywidgets\widgets\widget_selection.py:121, in _make_options(x)
118 x = x.items()
120 # only iterate once through the options.
--> 121 xlist = tuple(x)
123 # Check if x is an iterable of (label, value) pairs
124 if all((isinstance(i, (list, tuple)) and len(i) == 2) for i in xlist):
TypeError: 'Mbtree' object is not iterable
最後のエラーメッセージは、widget_selection.py
という、ipywidgets モジュールの中 で記述されている プログラムで発生 しています。このプログラムは自分で記述したものではなく、エラーメッセージを見ても エラーの原因がよくわかりません。そこで、エラーメッセージをさかのぼっていくと、Marubatsu_GUI クラスの __init__
メソッド内の self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
というプログラムを実行した結果エラーが発生していることが確認できます。
先程 Marubatsu_GUI クラスの __init__
メソッドの 最初の仮引数 を、局面と最善手・評価値の 対応表 を代入する bestmoves_and_score_by_board
から、Dropdown を登録する際に必要となる局面と最善手・評価値の 対応表の一覧 を代入する scoretable_dict
に変更 しましたが、上記の Marubatsu_GUI クラスの __init__
メソッドの処理では その変更が反映されていない ことがこのエラーが発生する原因です。
Marubatsu_GUI クラスの __init__
メソッドの修正
まず、Marubatsu_GUI クラスの __init__
メソッドを下記のプログラムのように修正する必要があります。
-
5 行目:仮引数
scoretable_dict
を追加する。なお、Marubatsu_GUI クラスのインスタンスは Marubatsu クラスのplay
メソッド内でのみ作成され、その際に必ずscoretable_dict
に対応する実引数を記述して呼び出すことにするので、デフォルト引数にする必要はない -
8 行目:
Mbtree
はインポートする必要がなくなったのでインポートしないようにした - 10 行目に記述されていた、ゲーム木のデータをファイルから読み込んでクラス属性
mbtree
に代入する処理は削除した。その理由はこの後で説明する -
10 行目:最初の実引数を
scoretable_dict
に修正する
1 from marubatsu import Marubatsu_GUI
2 from tkinter import Tk
3 import os
4
5 def __init__(self, mb, params, names, ai_dict, scoretable_dict, seed, size):
元と同じなので省略
6 super(Marubatsu_GUI, self).__init__()
7
8 from tree import Mbtree_GUI
9
10 self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
11
12 Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os
def __init__(self, mb, params, names, ai_dict, scoretable_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__
# 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__()
from tree import Mbtree_GUI
self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
Marubatsu_GUI.__init__ = __init__
修正箇所
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os
-def __init__(self, mb, params, names, ai_dict, seed, size):
+def __init__(self, mb, params, names, ai_dict, scoretable_dict, seed, size):
元と同じなので省略
super(Marubatsu_GUI, self).__init__()
- from tree import Mbtree, Mbtree_GUI
+ from tree import Mbtree_GUI
- if Marubatsu_GUI.mbtree is None:
- Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
- self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
+ self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
Marubatsu_GUI.__init__ = __init__
修正前のプログラムでは、__init__
メソッド内で 〇×ゲームのゲーム木のデータをファイルから読み込む必要 があり、その 所要時間が長い ので、同じデータを何度もファイルから読み込まなくても済むように クラス属性 mbtree
に読み込んだデータを代入するという工夫を行っていました。一方、局面と最善手・評価値の対応表の データは小さく、ファイルからそのデータを ほぼ一瞬で読み込むことができる のでそのような工夫を行う必要はありません。そのため、8 行目に記述されていたその処理を削除しました。
また、クラス属性 mbtree
はもう必要が無くなったので、marubatsu.py の Marubatsu_GUI クラスの定義の中の下記の 2 行目のプログラムは削除します。
class Marubatsu_GUI:
mbtree = None
略
Marubatsu_GUI クラスの update_gui
メソッドの修正
Marubatsu_GUI クラスの mbtree
属性が無くなった ので、Marubatsu_GUI クラスの中で、mbtree
属性を利用するプログラムを修正する必要 があります。
mbtree
属性は、ゲーム盤に着手を行った際に ゲーム盤の表示を更新するために 呼び出される update_gui
メソッドの下記の部分で利用されています
def update_gui(self):
略
if hasattr(self, "mbtree_gui"):
key = tuple(self.mb.records[:self.mb.move_count + 1])
self.mbtree_gui.selectednode = self.mbtree.nodelist_by_mb[key]
self.mbtree_gui.update_gui()
上記のプログラムでは、下記の手順でゲーム盤の下に表示する GUI の部分木の 選択されたノードを現在の局面を表すノードに変更 し、表示を更新するという処理を行います。
- ゲーム木を表す
self.mbtree
の中から、現在の局面を表すノードを探す -
そのノードを Mbtree_GUI クラスのインスタンスの
selectednode
属性に代入 する - GUI の部分木の表示を更新する
mbtree
属性がなくなったので、この処理を self.mbtree
利用せずに行う 必要があります。前回の記事で、selectednode
属性に代入する ノード のデータには、下記の属性のデータのみが必要 であることがわかっています。
- ノードの深さを表す
depth
属性 - 親ノードを表す
parent
属性
上記のうち、ノードの深さ は self.mb.move_count
によって計算できますが、局面を表す self.mb
のデータから、親ノードを計算するのは少々面倒 です。
親ノードのデータ は、下記のプログラムの Mbtree_GUI クラスの update_gui
メソッドの中で、深さが 6 より大きい場合 に中心となるノードを計算するために 必要 なので、深さが 6 以下 のノードの場合は 必要がありません。
def update_gui(self):
略
centernode = self.selectednode
while centernode.depth > 6:
centernode = centernode.parent
略
そこで、下記のプログラムのように、親ノードを計算する処理は一旦保留 し、親ノードを持たない self.mb
の ノードを作成 して selectednode
属性に代入することにします。
- 5 行目:7 行目の処理で Node クラスが必要になるが、marubatsu.py の先頭で Node クラスを mbtree.py からインポートすると循環参照が発生するので、ローカルなインポートで Node クラスをインポートする必要がある
-
7 行目:現在の局面を表すノードを作成し、
selectednode
属性に代入する。ただし、現時点では親ノードは存在しないものとしてノードを作成する - 8 行目:部分木の表示を更新する
1 def update_gui(self):
元と同じなので省略
2 self.update_widgets_status()
3
4 if hasattr(self, "mbtree_gui"):
5 from tree import Node
6
7 self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
8 self.mbtree_gui.update_gui()
9
10 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()
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
修正箇所
def update_gui(self):
元と同じなので省略
self.update_widgets_status()
if hasattr(self, "mbtree_gui"):
+ from tree import Node
- key = tuple(self.mb.records[:self.mb.move_count + 1])
- self.mbtree_gui.selectednode = self.mbtree.nodelist_by_mb[key]
+ self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
+ self.mbtree_gui.update_gui()
Marubatsu_GUI.update_gui = update_gui
Marubatsu クラスの play
メソッドの修正
次に、Marubatsu_GUI クラスの インスタンスを作成 する処理を行う play
メソッドを下記のプログラムのように修正する必要があります。
-
3 行目:デフォルト値を
None
とする仮引数scoretable_dict
を追加する -
7 行目:Marubatsu_GUI クラスのインスタンスを作成する際に、キーワード引数
scoretable_dict=scoretable_dict
を記述するように修正する
1 from random import random
2
3 def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=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,
7 scoretable_dict=scoretable_dict, seed=seed, size=size)
元と同じなので省略
8
9 Marubatsu.play = play
行番号のないプログラム
from random import random
def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=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,
scoretable_dict=scoretable_dict, seed=seed, size=size)
else:
mb_gui = None
self.restart()
return self.play_loop(mb_gui, params=params)
Marubatsu.play = play
修正箇所
from random import random
-def play(self, ai, ai_dict=None, params=None, names=None, verbose=True, seed=None, gui=False, size=3):
+def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict,
- seed=seed, size=size)
+ scoretable_dict=scoretable_dict, seed=seed, size=size)
元と同じなので省略
Marubatsu.play = play
上記の修正後に下記のプログラムを実行して、ゲーム盤に着手を行ってください。6 手目まで はゲーム盤の下の GUI の部分木が正しく更新 されますが、7 手目の着手を行うと 実行結果のような エラーが発生 します。
gui_play
の中ではキーワード引数 scoretable_dict
を記述せずに Marubatsu クラスのインスタンスの play
メソッドを呼び出しているので、仮引数 scoretable_dict
にはデフォルト値である None
が代入されます。そのため、その後の処理で Mbtree_GUI クラスのインスタンスを作成する際にはキーワード引数 scoretable_dict
には None
が代入されることになり、今回の記事で修正した Mbtree_GUI クラスの __init__
メソッドの最初の処理によって GUI の部分木には 2 つの項目を持つ Dropdown が作成されます。
gui_play()
実行結果
略
File c:\Users\ys\ai\marubatsu\123\tree.py:909, in Mbtree_GUI.update_gui(self)
907 maxdepth = 9
908 centernode = self.selectednode
--> 909 while centernode.depth > 6:
910 centernode = centernode.parent
911 self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
912 "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
AttributeError: 'NoneType' object has no attribute 'depth'
エラーの検証と修正方法
エラーメッセージから、Mbtree_GUI クラスの update_gui
メソッド内で、while centernode.depth > 6:
を実行した際に、centernode
に None
が代入されている ことがわかります。先ほど説明したように、selectednode
属性に代入する ノードを作成する際に親ノードを作成しなかった ので、parent
属性には None
が代入 されます。そのため、深さが 7 以上のノードが centernode
属性に代入された場合は、while 文のブロックの centernode = centernode.parent
が実行 されて centernode
に None
が代入 され、次の while 文の条件式が計算されると上記のエラーが発生します。
この問題を解決する方法としては、以下の 2 種類の方法が考えられます。
-
selectednode
属性に代入するノードを計算する際に、その 親ノードを作成する - 上記の
update_gui
で、親ノードの情報を使わずに 中心となるノードを計算するように修正する
後者のほうがプログラムの記述が簡単 なので、本記事では後者の方法で修正することにしますが、前者の方法が良いと思った方は実際に実装してみて下さい。
Mbtree_GUI クラスの update_gui
メソッドの修正
update_gui
メソッドでは、親ノードを辿ることで深さが 6 のノードを計算していますが、別の方法として、下記のプログラムのように records
属性に代入された棋譜のデータを利用 して、ゲーム開始時の局面から 6 手目までの着手を行う ことで、中心となるノードの局面を計算することができます。
- 2、3 行目:選択されたノードの深さが 6 以下の場合は、中心となるノードの局面を、選択されたノードの局面とする
-
4 ~ 7 行目:深さが 7 以上の場合は、5 行目でゲーム開始時の局面を表すデータを作成して
centermb
に代入し、選択された局面の棋譜のデータを利用して 6 手目までの着手を行い、その局面を中心となるノードの局面とする -
8 行目:中心となる局面のデータを
centernode.mb
からcentermb
に修正する
1 def update_gui(self):
元と同じなので省略
2 if self.selectednode.depth <= 6:
3 centermb = self.selectednode.mb
4 else:
5 centermb = Marubatsu()
6 for x, y in self.selectednode.mb.records[1:7]:
7 centermb.move(x, y)
8 self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
9 "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
元と同じなので省略
10
11 Mbtree_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
self.ax.clear()
self.ax.set_xlim(-1, self.width - 1)
self.ax.set_ylim(-1, self.height - 1)
self.ax.invert_yaxis()
self.ax.axis("off")
if self.selectednode.depth <= 4:
maxdepth = self.selectednode.depth + 1
elif self.selectednode.depth == 5:
maxdepth = 7
else:
maxdepth = 9
if self.selectednode.depth <= 6:
centermb = self.selectednode.mb
else:
centermb = Marubatsu()
for x, y in self.selectednode.mb.records[1:7]:
centermb.move(x, y)
self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
"bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
self.selectednode = self.mbtree.selectednode
self.mbtree.draw_subtree(centernode=self.mbtree.centernode, selectednode=self.selectednode,
show_bestmove=True, show_score=self.show_score,
ax=self.ax, maxdepth=maxdepth, size=self.size)
disabled = self.selectednode.parent is None
self.set_button_status(self.left_button, disabled=disabled)
disabled = self.selectednode.depth >= 6 or len(self.selectednode.children) == 0
self.set_button_status(self.right_button, disabled=disabled)
disabled = self.selectednode.parent is None or self.selectednode.parent.children.index(self.selectednode) == 0
self.set_button_status(self.up_button, disabled=disabled)
disabled = self.selectednode.parent is None or self.selectednode.parent.children[-1] is self.selectednode
self.set_button_status(self.down_button, disabled=disabled)
self.set_button_color(self.score_button, value=self.show_score)
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
- centernode = self.selectednode
- while centernode.depth > 6:
- centernode = centernode.parent
+ if self.selectednode.depth <= 6:
+ centermb = self.selectednode.mb
+ else:
+ centermb = Marubatsu()
+ for x, y in self.selectednode.mb.records[1:7]:
+ centermb.move(x, y)
- self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
+ self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
"bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
元と同じなので省略
Mbtree_GUI.update_gui = update_gui
上記の修正後に下記のプログラムを実行して 9 手目までの着手を行うと、実行結果のようにエラーが発生しなくなったことが確認できます。
gui_play()
実行結果
上記の実行結果に対して 様々な操作を行うと、上記のプログラムには リプレイ機能を用いるとエラーが発生する という問題があることがわかります。その問題については次回の記事で修正することするので、余裕がある方はそのエラーの原因と修正方法について考えておいてください。
今回の記事のまとめ
今回の記事では、Mbtree_GUI クラスのインスタンスを作成した後で、局面と最善手・評価値の対応表 を Dropdown で変更できるよう修正 しました。また、それにあわせて Marubatsu クラスの play
メソッドが動作する ように修正しました。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu_new.py | 今回の記事で更新した marubatsu.py |
tree_new.py | 今回の記事で更新した tree.py |
次回の記事