目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
マウス操作による中心となるノードを移動する GUI の問題点と改良
前回の記事で実装した、マウス操作による、中心となるノードの移動には、問題があることがわかりましたので修正します。実際に操作してみればわかる問題点だと思いますので、どのような問題があるかについて少し考えてみて下さい。
問題点
以前の記事で説明したように、深さ 6 のノードを中心とする部分木 は、最も深い深さ 9 のノードまでを描画することができる ので、GUI の操作によって 深さが 7 以上のノードが中心とならない ようにする処理を行いました。
→ ボタンで中心となるノードを子ノードに移動する場合は、中心となるノードの 深さが 1 ずつしか深くならない ので、深さ 6 のノードが中心の場合に → ボタンを押した際に移動できなくしても特に問題はありませんでした。
一方、マウスによって中心となるノードを移動 する場合は、2 つ以上深いノードに移動 することができてしまうので、深さが 7 以上のノードへ移動できなくする という処理には 問題があります。言葉の説明では意味がわからないと思いますので、具体例を示します。
下図は、深さ 5 のノードを中心とする部分木です。下図で、一番右の列に表示される、深さが 7 のノードの上でマウスを押しても 中心となるノードは移動しません。そのため、下図に表示されている 深さが 7 のノードの子ノードが表示される部分木 を表示するためには、深さが 6 のノードの上でマウスを押す必要があります。
具体的には、上図の深さが 7 の赤丸のノードの上でマウスを押しても表示は更新されず、赤丸の子ノードが表示される部分木を表示するためには、深さが 6 の青丸のノードの上でマウスを押す必要があります。下図は、上図で青丸のノードをマウスで押した場合の図で、赤丸のノードの子ノードが表示された部分木が表示されています。
赤丸の子ノードを表示するため に、赤丸以外 の青丸の ノードをマウスで押さなければならない というのは、わかりづらく不便 です。
この問題を修正する方法について少し考えてみて下さい。
問題の修正方法
以下は、この問題を修正する方法の一つです。他にも良い方法があるかもしれないので、もっと良い方法を思いついた方はその方法を実装してみて下さい。
- 部分木の中で、赤い枠を表示するノード を、選択されたノード と表現することにする
- すべてのノード を GUI の操作で 選択できる ようにする
- 選択されたノードの 深さが 6 以下 の場合は、これまでと同様 に、選択されたノードを中心とした部分木を描画する
- 選択されたノードの 深さが 7 以上 の場合は、その親ノードを辿った、深さが 6 の親ノードを中心とする部分木を描画 する
上記のように修正することで、深さが 7 以上のノードが選択された場合でも、深さが 6 から 9 までの部分木が描画されるようになります1。
わかりづらいかもしれませんが、この修正で部分木の 中心となるノードがなくなるわけではありません。また、選択されたノードの 深さが 6 以下の場合 は選択されたノードと中心となるノードは 同じになります。この修正は、GUI の 操作で中心となるノードではなく、赤枠で囲われた 選択されたノードを操作 できるようにするというものです
なお、この修正は、今回の記事の後半で行う、ゲーム木の生成の過程のアニメーション処理を行う際にも必要となる修正です。
centernode
属性の名前の修正
赤枠で表示されるノードは、これまでは中心となるノードと呼んでいたので Mbtree_GUI クラスの centernode
属性に代入していましたが、選択された(selected)ノード と呼び方を変えることにしたので、代入する 属性の名前 を selectednode
に変更することにします。
下記は、そのように Mbtree_GUI
クラスを修正したプログラムです。なお、修正は VSCode の シンボル名の変更の機能を使えば簡単なので、変更場所の説明は省略します。
修正したプログラム(長いのでクリックして開いてください)
from gui import GUI
import matplotlib.pyplot as plt
import ipywidgets as widgets
class Mbtree_GUI(GUI):
def __init__(self, mbtree, size=0.15):
self.mbtree = mbtree
self.size = size
self.width = 50
self.height = 64
self.selectednode = self.mbtree.root
super().__init__()
def create_widgets(self):
self.output = widgets.Output()
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.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
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 self.selectednode.depth < 6 and 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_help_button_clicked(b=None):
self.output.clear_output()
with self.output:
print("""操作説明
下記のキーとボタンで中心となるノードを移動できる。ただし、深さが 7 以上のノードへは移動できない
←、0 キー:親ノードへ移動
↑:一つ前の兄弟ノードへ移動
↓:一つ後の兄弟ノードへ移動
→:先頭の子ノードへ移動
テンキーで、対応するマスに着手が行われた子ノードへ移動する
ノードの上でマウスを押すことでそのノードへ移動する
""")
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.help_button.on_click(on_help_button_clicked)
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]()
elif self.selectednode.depth < 6:
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 node.depth <= 6 and 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)
def display_widgets(self):
hbox1 = widgets.HBox([self.label, self.up_button, self.label])
hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
self.label, self.help_button])
hbox3 = widgets.HBox([self.label, self.down_button, self.label])
display(widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas]))
def update_gui(self):
self.ax.clear()
self.ax.set_xlim(-1, self.width - 1)
self.ax.set_ylim(0, self.height)
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
self.mbtree.draw_subtree(self.selectednode, ax=self.ax, maxdepth=maxdepth)
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)
draw_subtree
の修正
部分木の中で、中心となるノード と、赤い枠を表示する 選択されたノード が 異なるノードになる場合が生じた ので、draw_subtree
には、中心となるノードと、選択されたノードを代入する 異なる仮引数を用意する必要 があります。また、draw_node
を呼び出す際に 強調して表示するノードを選択されたノードに変更 する必要があります。
下記は、そのように draw_subtree
を修正したプログラムです。
-
3 行目:選択されたノードを代入する、デフォルト値を
None
とする仮引数selectednode
を追加する -
6 行目:
draw_node
で強調して表示するノードを、selectednode
に変更する
1 from tree import Mbtree
2
3 def draw_subtree(self, centernode=None, selectednode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
4 else:
5 dx = 5 * node.depth
6 emphasize = node is selectednode
7 rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
8
9 Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
from tree import Mbtree
def draw_subtree(self, centernode=None, selectednode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
self.nodes_by_rect = {}
if centernode is None:
centernode = self.root
self.calc_node_height(maxdepth)
width = 5 * (maxdepth + 1)
height = centernode.height
parent = centernode.parent
if parent is not None:
height += (len(parent.children) - 1) * 4
parent.height = height
if ax is None:
fig, ax = plt.subplots(figsize=(width * size, height * size))
ax.set_xlim(0, width)
ax.set_ylim(0, height)
ax.invert_yaxis()
ax.axis("off")
nodelist = [centernode]
depth = centernode.depth
while len(nodelist) > 0 and depth <= maxdepth:
dy = 0
if parent is not None:
dy = parent.children.index(centernode) * 4
childnodelist = []
for node in nodelist:
if node is None:
dy += 4
childnodelist.append(None)
else:
dx = 5 * node.depth
emphasize = node is selectednode
rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
self.nodes_by_rect[rect] = node
dy += node.height
if len(node.children) > 0:
childnodelist += node.children
else:
childnodelist.append(None)
depth += 1
nodelist = childnodelist
if parent is not None:
dy = 0
for sibling in parent.children:
if sibling is not centernode:
sibling.height = 4
dx = 5 * sibling.depth
rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
self.nodes_by_rect[rect] = sibling
dy += sibling.height
dx = 5 * parent.depth
rect = parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
self.nodes_by_rect[rect] = parent
node = parent
while node.parent is not None:
node = node.parent
node.height = height
dx = 5 * node.depth
rect = node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
self.nodes_by_rect[rect] = node
Mbtree.draw_subtree = draw_subtree
修正箇所
from tree import Mbtree
-def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
+def draw_subtree(self, centernode=None, selectednode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
else:
dx = 5 * node.depth
- emphasize = node is centernode
+ emphasize = node is selectednode
rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
Mbtree.draw_subtree = draw_subtree
上記の修正後に、下記のプログラムを実行することで、実行結果から draw_subtree
が 中心となるノード と、選択されたノード が 異なる部分木を表示 できるようになったことを確認できます。
なお、下記のプログラムでは、下記のような部分木を表示しています。
- ルートノードを中心とする、深さ 1 までの部分木
- 選択されたノードは、深さ 1 の最初のノード
mbtree = Mbtree()
mbtree.draw_subtree(centernode=mbtree.root,
selectednode=mbtree.nodelist_by_depth[1][0], maxdepth=1)
実行結果
create_eventhandler
の修正
次に、create_eventhandler
を下記のプログラムのように、深さ 7 以上のノードを選択状態にできるように修正します。
- 3、23、27 行目:深さが 6 以下の場合に制限するという条件を、条件式から削除する
- 11 行目:操作説明のメッセージを修正する
1 def create_event_handler(self):
元と同じなので省略
2 def on_right_button_clicked(b=None):
3 if len(self.selectednode.children) > 0:
4 self.selectednode = self.selectednode.children[0]
5 self.update_gui()
元と同じなので省略
6 def on_help_button_clicked(b=None):
7 self.output.clear_output()
8 with self.output:
9 print("""操作説明
10
11 下記のキーとボタンで中心となるノードを移動できる。
12 ←、0 キー:親ノードへ移動
13 ↑:一つ前の兄弟ノードへ移動
14 ↓:一つ後の兄弟ノードへ移動
15 →:先頭の子ノードへ移動
16
17 テンキーで、対応するマスに着手が行われた子ノードへ移動する
18 ノードの上でマウスを押すことでそのノードへ移動する
19 """)
元と同じなので省略
20 def on_key_press(event):
元と同じなので省略
21 if event.key in keymap:
22 keymap[event.key]()
23 else:
24 try:
元と同じなので省略
25 def on_mouse_down(event):
26 for rect, node in self.mbtree.nodes_by_rect.items():
27 if rect.is_inside(event.xdata, event.ydata):
28 self.selectednode = node
29 self.update_gui()
30 break
元と同じなので省略
31
32 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_help_button_clicked(b=None):
self.output.clear_output()
with self.output:
print("""操作説明
下記のキーとボタンで中心となるノードを移動できる。
←、0 キー:親ノードへ移動
↑:一つ前の兄弟ノードへ移動
↓:一つ後の兄弟ノードへ移動
→:先頭の子ノードへ移動
テンキーで、対応するマスに着手が行われた子ノードへ移動する
ノードの上でマウスを押すことでそのノードへ移動する
""")
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.help_button.on_click(on_help_button_clicked)
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):
元と同じなので省略
def on_right_button_clicked(b=None):
- if self.selectednode.depth < 6 and len(self.selectednode.children) > 0:
+ if len(self.selectednode.children) > 0:
self.selectednode = self.selectednode.children[0]
self.update_gui()
元と同じなので省略
def on_help_button_clicked(b=None):
self.output.clear_output()
with self.output:
print("""操作説明
-下記のキーとボタンで中心となるノードを移動できる。ただし、深さが 7 以上のノードへは移動できない
+下記のキーとボタンで中心となるノードを移動できる。
←、0 キー:親ノードへ移動
↑:一つ前の兄弟ノードへ移動
↓:一つ後の兄弟ノードへ移動
→:先頭の子ノードへ移動
テンキーで、対応するマスに着手が行われた子ノードへ移動する
ノードの上でマウスを押すことでそのノードへ移動する
""")
元と同じなので省略
def on_key_press(event):
元と同じなので省略
if event.key in keymap:
keymap[event.key]()
- elif self.selectednode.depth < 6:
+ else:
try:
元と同じなので省略
def on_mouse_down(event):
for rect, node in self.mbtree.nodes_by_rect.items():
- if node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
+ if rect.is_inside(event.xdata, event.ydata):
self.selectednode = node
self.update_gui()
break
元と同じなので省略
Mbtree_GUI.create_event_handler = create_event_handler
上記の修正後に、下記のプログラムを実行すると、実行結果のように赤い枠のノードが表示されなくなります。これは、draw_subtree
を先程修正したにも関わらず、draw_subtree
を呼び出す処理を修正していないからです。
mbtree_gui = Mbtree_GUI(mbtree)
実行結果
update_gui
の修正
draw_subtree
は、update_gui
から呼び出しているので、update_gui
の中で、下記のような 中心となるノードを計算する必要 があります。
- 選択されたノードの 深さが 6 以下 の場合は、これまでと同様 に、選択されたノードを中心 とした部分木を描画する
- 選択されたノードの 深さが 7 以上 の場合は、その親ノードを辿った、深さが 6 の親ノードを中心 とする部分木を描画 する
そのような中心となるノードは、下記のプログラムの 2 ~ 4 行目の処理で計算できます。
-
2 行目:
centernode
に選択されたノードを代入する -
3、4 行目:
centernode
の深さが 6 より大きい場合に、centernode
に親ノードを代入する繰り返しの処理を行う -
5 行目:計算された
centernode
を実引数に記述してdraw_subtree
を呼び出す
上記の処理を行うと、centernode
に下記のようなノードが代入されます。
- 選択されたノードの深さが 6 以下の場合は、3 行目の条件式が 最初から
False
になる ため 4 行目の処理が行われない。従って、centernode
には選択されたノードが代入 される - 選択されたノードの深さが 7 以上の場合は
centernode
の深さが 6 になるまで 4 行目の処理が繰り返し行われる。そのため、centernode
には、深さが 6 の選択されたノードの親ノードが代入される
1 def update_gui(self):
元と同じなので省略
2 centernode = self.selectednode
3 while centernode.depth > 6:
4 centernode = centernode.parent
5 self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
6
7 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(0, self.height)
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
centernode = self.selectednode
while centernode.depth > 6:
centernode = centernode.parent
self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
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)
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
+ centernode = self.selectednode
+ while centernode.depth > 6:
+ centernode = centernode.parent
- self.mbtree.draw_subtree(self.selectednode, ax=self.ax, maxdepth=maxdepth)
+ self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
Mbtree_GUI.update_gui = update_gui
上記の修正後に、下記のプログラムを実行して深さ 7 以上のノードを選択状態にすると、実行結果のように、そのノードが赤い枠で表示され、そのノードの深さが 6 の親ノードが中心となる部分木が表示されるようになります。
mbtree_gui = Mbtree_GUI(mbtree)
実行結果
これで、ゲーム木を視覚化する GUI は完成ですが、他にも機能を付け加えたい人は実装してみて下さい。
ゲーム木の作成の過程の視覚化
Mbtree クラスでは、幅優先アルゴリズムによって、ゲーム木の作成を行いました。先程完成したゲーム木を視覚化する GUI では、完成したゲーム木を視覚化 することができますが、ゲーム木が どのように作成されたか を知ることはできません。そこで、幅優先アルゴリズムによって、ゲーム木のノードがどのような順番で作成されるかを視覚化することにします。実は、本記事で、ここまで部分木の視覚化の実装を行ってきた一つの理由は、ゲーム木の作成の過程の視覚化を行いたかったからです。
どのような方法で視覚化すればよいかについて少し考えてみて下さい。
〇×ゲームのリプレイ機能と同様の方法による視覚化
視覚化の方法として、〇×ゲームの GUI のリプレイ機能のように、下図の <<、<、>、>> の 4 つのボタンと IntSlider のウィジェットで、ゲーム木が作成される過程を表示するという方法が考えられますが、この方法はあまり良い方法ではないでしょう。
その理由の一つは、上記の UI では、ゲーム木が作成される過程の 表示を進めるため には、> ボタンをクリックする必要がある からです。〇× ゲームのゲーム木のノードは、以前の記事で説明したように、50 万以上もある ため、> ボタンをクリックして表示を進めるのは 現実的ではない でしょう。
また、他の理由としては、〇×ゲームのリプレイ機能の場合は、ゲームの経過を 1 手ずつ前後に移動しながら確認したい ので上記のような UI を採用しましたが、ゲーム木が作成される過程を表示する際に < ボタンをクリックして 1 つ前に戻る必要はあまりない でしょう。
ゲーム木が作成される過程を表示する場合は、自動的に表示が更新 されるという、アニメーション(動画)によって行うのが自然 だと思いますので、本記事では アニメーションによるゲーム木の生成の過程を表示する方法 について説明します。〇× ゲームのリプレイと同じ UI が良いと思った方は、そちらを採用して下さい。
Play ウィジェットによるアニメーション
アニメーションは、一定時間おき に、少しずつ内容が異なる画像を表示する ことで行います。原理的には、パラパラ漫画と同じ です。ノートや教科書の端に、パラパラ漫画によるアニメーションを描いたことがある人は多いのではないでしょうか?
一定時間おきに表示する画像 のことを フレーム(frame) と呼び、1 秒間に表示するフレームの数 を fps(frame per second)と呼びます。fps が高いほうが滑らかなアニメーションになり、テレビ番組や映画などでは約 30 fps、ゲームでは 60 fps が一般的なようです。
従って、プログラムでアニメーションを行うためには、一定時間おきに表示を更新する処理を行う 必要があります。
Play ウィジェット
ipywidgets には、アニメーションを行うために利用できる Play というウィジェットが用意されています。Play ウィジェットは、下図のような表示が行われるウィジェットで、下記のように、一定時間おき に value
属性を変化させる機能 を持ちます。
- 上図のような、3 つのボタンで構成されるウィジェットである
- 三角形のボタンをクリックすると、一定時間おきに、Play ウィジェットの
value
属性の値が変化 するようになる。その後、もう一度そのボタンをクリックするとvalue
属性の値が変化しなくなる -
value
属性には、初期値、最小値、最大値、加算される値 を設定することができる - 四角形のボタンをクリックすると、
value
属性の値が最小値になり、時間経過で変化しないようになる - 一番右のボタンをクリックして ON の状態にすると、
value
属性の値が最大値を超えた場合に初期値に戻り、ループするようになる
Play ウィジェットの詳細については、下記のリンク先を参照して下さい。
Play ウィジェットの属性
Play ウィジェットには、主に下記の属性があります。
属性 | 意味 | デフォルト値 |
---|---|---|
value |
一定時間おきに値が変化する属性 | 0 |
min |
value 属性の最小値 |
0 |
max |
value 属性の最大値 |
100 |
step |
時間経過による value 属性の変化量 |
1 |
interval |
value 属性が変化する間隔。単位はミリ秒 |
100 |
disabled |
True の場合にウィジェットを操作できなくなる |
False |
Play ウィジェットによるアニメーション処理
Play ウィジェットは、他のウィジェットと同様に observe
メソッドを使ってその 属性が変化した際に実行するイベントハンドラを結び付ける ことができ、その イベントハンドラに画像の表示を更新する処理を記述する ことで、アニメーションを行うことができます。
アニメーションの fps は、1 秒間に表示する画像の数なので、「1000 / interval
属性の値」という式で計算できます。例えば interval
属性に 100
を代入 した場合のアニメーションは、1000 / 100 = 10 fps のアニメーションになります。
逆に、fps から interval
属性の値を求める 場合は、「1000 / fps」 という式で計算できます。例えば 20 fps のアニメーションは、inteval
属性に 1000 / 20 = 50 を設定 します
下記は、赤い正方形の辺の長さを 1 から 10 まで増やしながら表示する というアニメーションを、10 fps で行うプログラムです。実行すると、実行結果の左図のような表示が行われ、三角形のボタンをクリックすると、右図のようなアニメーションが表示されます。
なお、イベントハンドラは value
属性が変更された際に呼び出され、value
属性の値 は Play ウィジェットの 三角形のボタンをクリックするまでは変化しない ので、下記のプログラムを 実行した直後 は、Figure には何も表示されません。
- 3 ~ 5 行目:ヘッダ2の無い Figure を作成し、Axes の軸を表示しないようにする
-
7 行目:
value
属性の初期値が 1、最小値が 1、最大値が 10、fps が 10 になるようにinterval
属性が100
の Play ウィジェットを作成しplay
に代入する -
9 ~ 17 行目:Play ウィジェットの
value
属性が変化した際に呼び出されるイベントハンドラを定義する - 10 ~ 13 行目:それまでに Axes に表示されていた内容をクリアし、Axes の表示範囲を設定し、軸を表示しないようにする
-
15 行目:更新された Play ウィジェットの
value
属性の値をローカル変数value
に代入する。なお、この部分にはvalue = play.value
を記述しても良い -
16、17 行目:(0, 0) を頂点とし、縦横の辺の長さが
value
の赤い正方形の Artist を作成し、Axes に登録して表示する -
19 行目:Play ウィジェットに、
value
属性の値が変更された際に呼び出されるイベントハンドラを結び付ける
1 import matplotlib.patches as patches
2
3 fig, ax = plt.subplots(figsize=(3, 3))
4 fig.canvas.header_visible = False
5 ax.axis("off")
6
7 play = widgets.Play(value=1, min=1, max=10, intervale=100)
8
9 def on_play_changed(changed):
10 ax.clear()
11 ax.set_xlim(0, 10)
12 ax.set_ylim(0, 10)
13 ax.axis("off")
14
15 value = changed["new"]
16 rect = patches.Rectangle(xy=(0, 0), width=value, height=value, fc="red")
17 ax.add_artist(rect)
18
19 play.observe(on_play_changed, names="value")
20
21 display(play)
行番号のないプログラム
import matplotlib.patches as patches
fig, ax = plt.subplots(figsize=(3, 3))
fig.canvas.header_visible = False
a x.axis("off")
play = widgets.Play(value=1, min=1, max=10, intervale=100)
def on_play_changed(changed):
ax.clear()
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
value = changed["new"]
rect = patches.Rectangle(xy=(0, 0), width=value, height=value, fc="red")
ax.add_artist(rect)
play.observe(on_play_changed, names="value")
display(play)
実行結果
上記のプログラムを実行して、Play ウィジェットの三角形のボタンをクリックすると、下記のような処理が行われるため、上図右のようなアニメーションが行われます。なお、上図右では、アニメーションがリピートしますが、実際に表示されるアニメーション では、三角形のボタンを押しただけでは アニメーションのリピートは行われません。Play ウィジェットの右のボタンをクリックしてから三角形のボタンをクリックすることでアニメーションがリピートするようになります。
-
play.value
が 100 ミリ秒おきに 1 増えるようになる。 -
value
属性の値が変更されると、on_play_changed
が呼び出される -
on_play_changed
では、value
属性の値を辺の長さとする赤い正方形を描画する - 上記から、1000 / 100 = 10 fps のアニメーションが表示される
matplotlib を利用したアニメーションは、matplotlib の ArtistAnimation や FuncAnimation を使って作成することもできます。ただし、これらを使ってアニメーションを作成する場合は、アニメーションの すべてのフレームの画像を用意する必要 があります。〇×ゲームのゲーム木には 50 万以上のノードが存在するので、〇×ゲームのゲーム木の生成過程を表すアニメーションを作成するためには、50 万以上のフレームの画像を作成する必要があるため、アニメーションを開始する前 に、かなりの時間が必要 になります。そのため、本記事ではそれらは採用しません。
なお、ArtistAnimation などには、アニメーションの作成に時間がかかるという欠点がありますが、動画ファイルとして保存することができるなどの利点があります。
matplotlib の ArtistAnimation や FuncAnimation を使ったアニメーションに興味がある方は、下記のリンク先を参照して下さい。本記事でも、必要があればそれらの使い方を今後紹介するかもしれません。
具体的な手順は長くなるので説明しませんが、本記事の中で先程表示したアニメーションの画像は、FuncAnimation を使って作成した mp4 の動画を、アニメーション GIF の形式に変換したものです。アニメーション GIF に変換した理由は、Quiita には、mp4 の動画ファイルをアップロードすることができないからです。
なお、ArtistAnimation や FuncAnimation は、Figure のアニメーションを作成するためのものなので、Play ウィジェットや IntSlider ウィジェットなどをアニメーションに含めることはできません。以後の記事のアニメーション画像に Play ウィジェットなどを表示しないのはそのためです。
Play ウィジェットと IntSlider ウィジェットのリンク
Play ウィジェットだけ では、アニメーションの どのフレームが現在描画されているか が わからない という問題があります。この問題を解決する方法の一つに、Play ウィジェット と IntSlider ウィジェット を リンク して連動させるというものがあります。
ipywidgets のウィジェットは、jslink
という関数によって、複数のウィジェットの属性をリンクして連動する ことができます。具体的には、下記のプログラムのように記述することで、リンクさせた片方のウィジェットの 属性の値が変更される と、自動的にもう片方の属性の値がその値に変更される ようになります。
widgets.jslink((ウィジェット1, 属性名1), (ウィジェット2, 属性名1))
下記は、先程のプログラムに IntSlider を加え、Play ウィジェットと IntSlider の value
属性をリンク して連動させるようにしたものです。
-
3 行目:Play と同じ
value
、min
、max
属性の値を設定した IntSlider を作成する -
4 行目:Play と IntSlider の
value
属性をリンクする - 7 行目:Play と IntSlider を HBox を使って横に並べて配置して表示する
元と同じなので省略
1
2 play = widgets.Play(value=1, min=1, max=10, interval=100)
3 slider = widgets.IntSlider(value=1, min=1, max=10)
4 widgets.jslink((play, "value"), (slider, "value"))
5
元と同じなので省略
6
7 display(widgets.HBox([play, slider]))
行番号のないプログラム
fig, ax = plt.subplots(figsize=(3, 3))
fig.canvas.header_visible = False
ax.axis("off")
play = widgets.Play(value=1, min=1, max=10, interval=100)
slider = widgets.IntSlider(value=1, min=1, max=10)
widgets.jslink((play, "value"), (slider, "value"))
def on_play_changed(changed):
ax.clear()
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis("off")
value = changed["new"]
rect = patches.Rectangle(xy=(0, 0), width=value, height=value, fc="red")
ax.add_artist(rect)
play.observe(on_play_changed, names="value")
display(widgets.HBox([play, slider]))
修正箇所
元と同じなので省略
play = widgets.Play(value=1, min=1, max=10, interval=100)
+slider = widgets.IntSlider(value=1, min=1, max=10)
+widgets.jslink((play, "value"), (slider, "value"))
元と同じなので省略
-display(play)
+display(widgets.HBox([play, slider]))
実行結果(Figure の部分は省略します)
上記のプログラムを実行すると、下図のように Play ウィジェットの右に IntSlider が表示されるようになます。また、Play ウィジェットと IntSlider ウィジェットが下記のように連動するようになります。
- Play ウィジェットの三角形のボタンをクリックすると、IntSlider が Play ウィジェットの
value
属性の値に連動して変化するようになる - IntSlider をドラッグして変更すると、Play ウィジェットの
value
属性の値が連動して変更されてイベントハンドラが呼び出されるようになり、Figure の表示が更新される
さらに、< ボタンや > ボタンを加えることで、〇×ゲームのリプレイ機能のように、アニメーションのフレームを 1 つずつ前後にずらすことができるようになります。
jslink
による属性のリンクは、イベントハンドラの仕組みを使って行うこともできます。例えば、Play の value
属性が変更された際に呼び出されるイベントハンドラと、Slider の value
属性が変更された際に呼び出されるイベントハンドラに下記のように記述します。なお、イベントハンドラとウィジェットの結び付けは省略します。
def on_play_changed(changed):
slider.value = changed["new"]
def on_slider_changed(changed):
play.value = changed["new"]
ただし、イベントハンドラの仕組みを使った連動には、以下のような欠点があるので、属性の値を連動するだけ の場合は、jslink
を利用したほうが良い でしょう。
- イベントループからイベントハンドラが呼び出されるという仕組みから、連動が瞬時に行われず、ほんの少し時間が経過してから連動が行われる
-
jslink
による属性のリンクは、イベントハンドラの仕組みを利用していないので、属性の値が変更された瞬間にもう片方の属性の値が変更される -
jslink
による属性のリンクは、2 つのイベントハンドラを定義する必要がなく、1 行で簡潔に記述できる
ただし、jslink
で行うことができるのは、属性の値の連動だけ です。属性の値が変更した際に、画像の表示などの、属性の値の連動以外の処理を行いたい場合は、イベントハンドラを利用する必要がある点に注意して下さい。
jslink
は、2 つの属性を双方向にリンクしますが、片方向のみのリンクを行う jsdlink
3という関数もあります。
双方向と片方向の違いがわらない人がいるかもしれないので、具体例を挙げます。
双方向のリンクでは、属性 A と B があった時に、以下のような連動が行われます。
- 属性 A が変化した場合に、属性 B の値が変化する
- 属性 B が変化した場合に、属性 A の値が変化する
片方向のリンクでは、属性 A と B があった時に、以下のような連動が行われます。
- 属性 A が変化した場合に、属性 B の値が変化する
- 属性 B が変化した場合に、属性 A の値は変化しない
なお、イベントハンドラの仕組みで属性のリンクを記述する場合は、片方のウィジェットのイベントハンドラのみを記述することで片方向のリンクになります。
jslink
、jsdlink
の詳細については、下記のリンク先を参照して下さい。
アニメーションの仕様
アニメーションを行う方法がわかったので、ゲーム木の生成の過程の アニメーションの仕様を決める ことにします。どのような仕様が良いかについて少し考えてみて下さい。
一つの方法として、ルートノードのみのゲーム木から始め、Play ウィジェットの value
属性の値が 1 増える ごとに、幅優先アルゴリズムと同じ順番で ゲーム木にノードを追加 し、それを表示するという方法が考えられるでしょう。ただし、本記事ではこの方法は、下記の理由から採用しないことにします。
- 上記のアニメーションを行うためには、
value
属性の値が 1 増えた際に、ゲーム木に対して、幅優先アルゴリズムと同じ順番で、ノードを 1 つだけ追加するという処理 を行う必要がある。そのような処理を、これまでに説明した方法で行うのは困難である - 上記の方法でアニメーションを行った場合に、IntSlider の値を大きく増やす と、処理に時間がかかってしまう。例えば、IntSlider の値を 0 から 約 50 万 に増やした場合、50 万個のノードをゲーム木に追加 する必要があるため、かなりの時間が必要 になる。このことは、ゲーム木を作成する際に 30 秒以上の時間が必要になることから明らかである
- IntSlider の値を減らした場合 に、ゲーム木を 一から作り直す 必要が生じる
もう一つの方法としては、完成したゲーム木 に対して、Play ウィジェットの value
属性の値が 1 増えるごとに、幅優先アルゴリズムと 同じ順番 でゲーム木に 追加したノード を、赤い枠で選択状態にして表示する という方法が考えられます。具体的には、下記のような方法でアニメーションを表示します。
-
アニメーションのフレームを 0 番から数えることにする
-
0 番のフレームには、ルートノードが選択状態になった部分木を描画する
-
n 番のフレームには、ゲーム木に n 番目に追加されたノードが中心かつ、選択状態になった部分木を描画する
このアニメーションでは、表示される 部分木の赤い枠のノードを見る ことで、どのような順番でゲーム木にノードが追加されていくか を知ることができます。
また、この方法では、ゲーム木を 新しく作り直す必要はない ので、IntSlider でアニメーションのフレームを大きく増やしても、表示に大きな時間がかかるようなことはありません。
ただし、上記の方法で本当にわかりやすいアニメーションが表示できるという保証はありませんので、上記の方針でアニメーションを実装した結果、わかりづらいことが判明した時点で、必要に応じて仕様を修正することにします。
Mbtree
クラスの修正
上記のようなアニメーションを行うためには、ゲーム木の 各ノード が、ゲーム木に追加された順番を記録 する必要があります。そのような情報は、list の要素 に、ゲーム木に 追加された順番でノードを代入 することで表現することができます。
そこで、Mbtree クラスに、その情報を代入する nodelist
という属性を追加することにします。そうすることで、Play ウィジェットの value
属性 の値を nodelist
のインデックス とすることで、value
番のフレーム に表示する部分木の 選択されたノードを計算 することができるようになります。
幅優先探索では、深さが浅い順にノードを追加 し、各深さのノードの情報 は、ゲーム木に 追加された順番 で Mbtree クラスの nodelist_by_depth
属性に代入 されています。そのため、nodelist
は、下記のプログラムのように記述することで計算できます。
-
5 行目:
nodelist
属性を空の list で初期化する -
8 行目:
nodelist
に、深さが浅い順にnodelist_by_depth
に記録されたノードの list を+=
演算子で 拡張 する
1 from marubatsu import Marubatsu
2
3 def create_tree_by_bf(self):
元と同じなので省略
4 self.nodenum = 0
5 self.nodelist = []
6 for nodelist in self.nodelist_by_depth:
7 self.nodenum += len(nodelist)
8 self.nodelist += nodelist
9 print(f"total node num = {self.nodenum}")
10
11 Mbtree.create_tree_by_bf = create_tree_by_bf
行番号のないプログラム
from marubatsu import Marubatsu
def create_tree_by_bf(self):
# 深さ 0 のノードを、子ノードを作成するノードのリストに登録する
nodelist = [self.root]
depth = 0
# 各深さのノードのリストを記録する変数を初期化する
self.nodelist_by_depth = [ nodelist ]
# 深さ depth のノードのリストが空になるまで繰り返し処理を行う
while len(nodelist) > 0:
childnodelist = []
for node in nodelist:
if node.mb.status == Marubatsu.PLAYING:
node.calc_children()
childnodelist += node.children
self.nodelist_by_depth.append(childnodelist)
nodelist = childnodelist
depth += 1
print(f"{len(nodelist):>6} depth {depth} node created")
self.nodenum = 0
self.nodelist = []
for nodelist in self.nodelist_by_depth:
self.nodenum += len(nodelist)
self.nodelist += nodelist
print(f"total node num = {self.nodenum}")
Mbtree.create_tree_by_bf = create_tree_by_bf
修正箇所
from marubatsu import Marubatsu
def create_tree_by_bf(self):
元と同じなので省略
self.nodenum = 0
+ self.nodelist = []
for nodelist in self.nodelist_by_depth:
self.nodenum += len(nodelist)
+ self.nodelist += nodelist
print(f"total node num = {self.nodenum}")
Mbtree.create_tree_by_bf = create_tree_by_bf
上記の修正後に、下記のプログラムで Mbtree クラスのインスタンスを作成しなおし、2 行目で nodelist
属性の要素の数 を表示すると、total node num の行で表示された値と同じ値が表示 されることが確認できます。また、3 行目から nodelist
の先頭の要素 に、ルートノードが代入されている ことが確認できます。
mbtree = Mbtree()
print(len(mbtree.nodelist))
print(mbtree.nodelist[0] is mbtree.root)
実行結果
9 depth 1 node created
72 depth 2 node created
504 depth 3 node created
3024 depth 4 node created
15120 depth 5 node created
54720 depth 6 node created
148176 depth 7 node created
200448 depth 8 node created
127872 depth 9 node created
0 depth 10 node created
total node num = 549946
549946
True
Mbtree_Anim
クラスの定義
アニメーションの処理 は、Play ウィジェットや IntSlider ウィジェットを利用した GUI のプログラム なので、Mbtree_GUI クラスと同様に、GUI クラスを継承 した Mbtree_Anim というクラスを定義して実装することにします。
下記は Mbtree_Anim クラスの定義です。Mbtree_Anim では ゲーム木の部分木を描画する ので、__init__
メソッドには、下記のプログラムのように、Mbtree_GUI クラスの __init__
メソッドと ほぼ同様の処理 を記述します。
ただし、Mbtree_Anim クラスで表示する部分木の 選択されたノード は、後述の update_gui
メソッドの中で、アニメーションの フレームの番号 を表す Play ウィジェットの value
属性から計算する ことになるので、Mbtree_GUI クラスの __init__
メソッドで行っていた、selectednode
属性にルートノードを代入する self.selectednode = self.mbtree.root
という処理を 記述する必要ありません。
__init__
メソッド以外のメソッドの定義は、この後で行います。
class Mbtree_Anim(GUI):
def __init__(self, mbtree, size=0.15):
self.mbtree = mbtree
self.size = size
self.width = 50
self.height = 64
super().__init__()
def create_widgets(self):
pass
def display_widgets(self):
pass
def create_event_handler(self):
pass
def update_gui(self):
pass
Mbtree_GUI クラスの __init__
メソッドからの修正箇所
def __init__(self, mbtree, size=0.15):
self.mbtree = mbtree
self.size = size
self.width = 50
self.height = 64
- self.selectednode = self.mbtree.root
super().__init__()
create_widgets
メソッドの定義
Mbtree_Anim クラスでは、Play ウィジェット、IntSlider ウィジェット、Figure を表示するので、create_widgets
メソッドは下記のように定義します。その際に、アニメーションの fps を決める必要がありますが、今回の実装ではアニメーションがあまり速くなりすぎないように、2 fps に設定しました。
-
2 行目:ゲーム木の ノードの数 は
self.mbtree.nodenum
に代入されているので、Play ウィジェットのvalue
属性の最大値 を表すmax
属性には その値から 1 を引いた値を代入 する。list のインデックスが 0 から数え始めるので 1 を引く必要がある点に注意する事。また、fps が 2 のアニメーションを行うために、interval
に 500 を代入した -
3 行目:IntSlider ウィジェットは Play ウィジェットとリンクさせるので、
max
属性に同じ値を代入 する。また、Intslider の左に frame という文字列が表示 されるようにするために、description
属性に"frame"
を代入 した -
4 行目:Play と IntSlider ウィジェットの
value
属性をリンク させる - 6 ~ 13 行目:Figure を作成する。この部分は Mbtree_GUI と全く同じである
1 def create_widgets(self):
2 self.play = widgets.Play(max=self.mbtree.nodenum - 1, interval=500)
3 self.slider = widgets.IntSlider(max=self.mbtree.nodenum - 1, description="frame")
4 widgets.jslink((self.play, "value"), (self.slider, "value"))
5
6 with plt.ioff():
7 self.fig = plt.figure(figsize=[self.width * self.size,
8 self.height * self.size])
9 self.ax = self.fig.add_axes([0, 0, 1, 1])
10 self.fig.canvas.toolbar_visible = False
11 self.fig.canvas.header_visible = False
12 self.fig.canvas.footer_visible = False
13 self.fig.canvas.resizable = False
14
15 Mbtree_Anim.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
self.play = widgets.Play(max=self.mbtree.nodenum - 1, interval=500)
self.slider = widgets.IntSlider(max=self.mbtree.nodenum - 1, description="frame")
widgets.jslink((self.play, "value"), (self.slider, "value"))
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
Mbtree_Anim.create_widgets = create_widgets
value
、min
、step
属性は、デフォルト値である 0
、0
、1
で良いので上記では記述しませんでしたが、それらの属性の値を明確にしたい人は下記のプログラムのように記述して下さい。
self.play = widgets.Play(value=0, min=0, max=self.mbtree.nodenum - 1,
step=1, interval=500)
self.slider = widgets.IntSlider(value=0, min=0, max=self.mbtree.nodenum - 1,
step=1, description="frame")
display_widgets
メソッドの定義
display_widgets
メソッドは下記のプログラムのように定義します。
- 2 行目:Play と IntSlider ウィジェットを横に配置した Hbox を作成する
- 3 行目:上記の Hbox と Figure を縦に配置して表示する
def display_widgets(self):
hbox = widgets.HBox([self.play, self.slider])
display(widgets.VBox([hbox, self.fig.canvas]))
Mbtree_Anim.display_widgets = display_widgets
update_gui
メソッドの定義
update_gui
は下記のプログラムのように、Play ウィジェットの value
番目にゲーム木に挿入されたノード を 中心かつ選択されたノードとする部分木 を描画するように定義します。
-
2、3 行目:
value
番目にゲーム木に挿入されたノード は、Mbtree クラスのnodelist
属性 に代入された list のvalue
番のインデックスの要素に代入されているので、self.mbtree.nodelist[self.play.value]
で計算できる。それをselectednode
属性とcenternode
属性に代入する
1 def update_gui(self):
元と同じなので省略
2 self.selectednode = self.mbtree.nodelist[self.play.value]
3 self.centernode = self.selectednode
4 if self.selectednode.depth <= 4:
5 maxdepth = self.selectednode.depth + 1
6 elif self.selectednode.depth == 5:
7 maxdepth = 7
8 else:
9 maxdepth = 9
10 self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
11
12 Mbtree_Anim.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
self.ax.clear()
self.ax.set_xlim(-1, self.width - 1)
self.ax.set_ylim(0, self.height)
self.ax.invert_yaxis()
self.ax.axis("off")
self.selectednode = self.mbtree.nodelist[self.play.value]
self.centernode = self.selectednode
if self.selectednode.depth <= 4:
maxdepth = self.selectednode.depth + 1
elif self.selectednode.depth == 5:
maxdepth = 7
else:
maxdepth = 9
self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
Mbtree_Anim.update_gui = update_gui
修正箇所(Mbtree_GUI からの修正箇所です)
def update_gui(self):
元と同じなので省略
+ self.selectednode = self.mbtree.nodelist[self.play.value]
+ self.centernode = self.selectednode
if self.selectednode.depth <= 4:
maxdepth = self.selectednode.depth + 1
elif self.selectednode.depth == 5:
maxdepth = 7
else:
maxdepth = 9
self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
Mbtree_Anim.update_gui = update_gui
上記のプログラムでは、selectednode
属性と、centernode
属性に値を代入しましたが、それらの属性の値は update_gui
メソッドの中でしか利用しない ので、ローカル変数に代入してもかまいません。ただし、そのようにすると Mbtree_GUI
クラスの update_gui
メソッドをコピーして Mbtree_Anim
クラスの update_gui
クラスを定義する際に、修正箇所が増えてしまうので、本記事では採用しませんでした。
create_event_handler
メソッドの定義
create_event_handler
には、下記のプログラムのように、Play ウィジェットの value
属性の値が変更された場合 に呼び出される イベントハンドラを定義 し、Play ウィジェットに結びつけます。また、このイベントハンドラが行う処理は、Figure の表示の更新だけ なので、update_gui
を呼び出す処理のみを記述します。
def create_event_handler(self):
def on_play_changed(changed):
self.update_gui()
self.play.observe(on_play_changed, names="value")
Mbtree_Anim.create_event_handler = create_event_handler
処理の確認
上記の修正後に、下記のプログラムを実行すると、実行結果の左図のように ルートノード が 中心かつ、選択された部分木 が表示されます。また、三角形のボタンをクリックすると、右図のようにゲーム木に 登録された順 で、赤い枠のノードが表示 される アニメーション が行われることと、IntSlider をドラッグ することで、アニメーションの 任意のフレームの表示に移動できる ことを確認して下さい。
mbtree_anim = Mbtree_Anim(mbtree)
実行結果(右図は 0 ~ 19 フレームまでのアニメーションの繰り返しです)
上記のプログラムでアニメーションを行う際に、たまに表示が固まる場合があり、そのような場合は、しばらくすると固まっていた間のフレームの表示が飛ばされて、その先のフレームが表示されます。
この現象はおそらく、以前の記事のノートで説明した、利用できなくなったオブジェクトのデータをメモリから削除する、ガーベジコレクションという処理を Python が行っていることが原因である可能性が高いのではないかと思います。
Play ウィジェットは、interval で指定した時間が経過した際に、ガーベジコレクションなどの理由でイベントハンドラが実行できなかった場合は、その時の value
の値に対するイベントハンドラの 呼び出しの処理が飛ばされる ようになっているようです。そのため、ガーベジコレクションが発生した場合は、その間のフレームの表示が飛ばされて、その先のフレームが表示されます。
ガーベジコレクションは、いつ、どれくらいの長さで行われるかが予測できないので、アニメーションの処理の最中に行われると、アニメーションのフレームのいくつかが飛んでしまうことになります。
なお、Python のガーベジコレクションの仕様が良くわからなかったので、上記のガーベジコレクションに関する説明は間違っているかもしれません。間違っている場合は、コメントで指摘していただけると嬉しいです。
今回の記事のまとめ
今回の記事では、マウス操作による中心となるノードの移動に関する処理の修正と、ゲーム木の作成の過程を表示するアニメーションの実装を行いました。
ただし、今回作成したアニメーションにはいくつかの問題点があるので、ゲーム木の生成過程があまりわかりやすく表示されません。次回の記事ではそれらを修正することにします。余裕がある方は、どのような問題点があるかについて考えてみて下さい。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
tree_new.py | 今回の記事で更新した tree.py |
次回の記事