目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
ゲーム木を視覚化する Mbtree_GUI クラスの作成
前回の記事で、GUI に関する処理を行う基底クラスとなる GUI クラスを作成しましたので、今回の記事ではそれを継承した、ゲーム木の視覚化を行う Mbtree_GUI クラスを定義します。ゲーム木の視覚化する GUI の仕様については、前回の記事を参照して下さい。
draw_tree
に関する修正
Mbtree_GUI を定義する前に、draw_tree
に関する修正を行うことにします。
draw_tree
は最初は startnode
で指定したノードからはじまり(start)、指定した深さまでのノードの部分木を描画していたので、仮引数の名前を startnode
にしましたが、その後の修正によって、startnode
の親ノードも描画する ようになったため、startnode
という名前を変えたほうが良いでしょう。現状では、draw_tree
は、startnode
を中心(center)とした部分木を描画 するので、startnode
を centernode
という名前に修正することにします。
また、draw_tree
が行う処理は、ゲーム木の 部分木(subtree)の描画 ですが、draw_tree
という名前では、ゲーム木全体を描画するように勘違いされる可能性が高いでしょう。そこで、メソッドの名前を draw_subtree
に修正することにします。
また、以後は draw_tree
が行う処理を、「centernode
を中心とする部分木を描画する」のように表記することにします。
なお、一般的には仮引数の名前やメソッドの名前を変更すると、そのメソッドを利用する他の関数やメソッドを修正する必要が生じますが、幸いなことに、現時点では他の関数やメソッドから draw_tree
を呼び出す処理をどこにも記述していないので、上記の修正を行っても問題は発生しません。
下記は、そのように draw_tree
を修正したプログラムです。startnode
を centernode
に修正する際は、以前の記事で説明した、VSCode の シンボル名の変更の機能を使うと簡単に行える ので、修正した場所を行番号で示す説明は省略します。
修正したプログラム
from tree import Mbtree
import matplotlib.pyplot as plt
def draw_subtree(self, centernode=None, size=0.25, lw=0.8, maxdepth=2):
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
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
node.draw_node(ax=ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=dy)
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
sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
dy += sibling.height
dx = 5 * parent.depth
parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
node = parent
while node.parent is not None:
node = node.parent
node.height = height
dx = 5 * node.depth
node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
Mbtree.draw_subtree = draw_subtree
修正箇所
from tree import Mbtree
import matplotlib.pyplot as plt
-def draw_tree(self, startnode=None, size=0.25, lw=0.8, maxdepth=2):
+def draw_subtree(self, centernode=None, size=0.25, lw=0.8, maxdepth=2):
- if startnode is None:
+ if centernode is None:
- startnode = self.root
+ centernode = self.root
self.calc_node_height(maxdepth)
width = 5 * (maxdepth + 1)
- height = startnode.height
+ height = centernode.height
- parent = startnode.parent
+ parent = centernode.parent
if parent is not None:
height += (len(parent.children) - 1) * 4
parent.height = height
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 = [startnode]
+ nodelist = [centernode]
- depth = startnode.depth
+ depth = centernode.depth
while len(nodelist) > 0 and depth <= maxdepth:
dy = 0
if parent is not None:
- dy = parent.children.index(startnode) * 4
+ 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
node.draw_node(ax=ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=dy)
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 startnode:
+ if sibling is not centernode:
sibling.height = 4
dx = 5 * sibling.depth
sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
dy += sibling.height
dx = 5 * parent.depth
parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
node = parent
while node.parent is not None:
node = node.parent
node.height = height
dx = 5 * node.depth
node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
Mbtree.draw_subtree = draw_subtree
上記の修正後に、下記のプログラムでルートノードを中心とする、深さ 2 までの部分木を描画することで、実行結果のように draw_subtree
が正しく動作することが確認できます。
mbtree = Mbtree()
mbtree.draw_subtree(mbtree.root, maxdepth=2)
実引数を省略した場合は、ルートノードを中心とする深さ 2 までの部分木を描画するように draw_subtree
を定義したので、上記のプログラムは下記のプログラムのように記述できますが、そのことを忘れている人がいるかもしれないので、上記では実引数を記述しました。なお、実行結果は上記と同じなので省略します。
mbtree.draw_subtree()
GUI クラスを継承した Mbtree_GUI クラスの定義
GUI クラスを継承した Mbtree_GUI クラスは、下記のプログラムのように定義します。
- 3 行目:GUI クラスを基底クラスとする、Mbtree_GUI クラスを定義する
-
4 行目:Mbtree_GUI クラスでは、〇×ゲームのゲーム木に対する処理を行う必要がある ので、〇×ゲームのゲーム木を表す Mbtree クラスのインスタンスの情報が必要 になる。そこで、その情報を代入する
mbtree
という仮引数を__init__
メソッドに追加する。また、Mbtree_GUI で表示する部分木の画像を表す Figure のサイズを調整 できるように、size
という仮引数を追加することにする -
5、6 行目:
mbtree
とsize
を、同じ名前の属性に代入する -
7 行目:基底クラスである GUI クラスの
__init__
メソッドの処理をsuper
を使って呼び出すことで、GUI に共通する初期化処理 を行う -
9 ~ 22 行目:
create_widgets
などの、GUI クラスの 抽象メソッド と同じ名前のメソッドを定義して オーバーライド する。なお、これらのメソッドの処理はこの後で記述する
1 from gui import GUI
2
3 class Mbtree_GUI(GUI):
4 def __init__(self, mbtree, size=1):
5 self.mbtree = mbtree
6 self.size = size
7 super().__init__()
8
9 def create_widgets(self):
10 pass
11
12 def create_event_handler(self):
13 pass
14
15 def display_widgets(self):
16 pass
17
18 def update_gui(self):
19 pass
20
21 def update_widgets_status(self):
22 pass
行番号のないプログラム
from gui import GUI
class Mbtree_GUI(GUI):
def __init__(self, mbtree, size=1):
self.mbtree = mbtree
self.size = size
super().__init__()
def create_widgets(self):
pass
def create_event_handler(self):
pass
def display_widgets(self):
pass
def update_gui(self):
pass
def update_widgets_status(self):
pass
super().__init__()
を __init__
メソッドのどこで呼び出すかは、状況によって異なります。Mbtree_GUI クラスの場合は、self.mbtree
や self.size
などの情報を、super().__init__()
の中から呼び出されるメソッドで利用する必要がある(例えば、Figure を作成する際に self.size
の情報が必要になります)ので、self.size = size
などの処理を、super().__init__()
の前に記述する必要があります。
逆に、super().__init__()
の中で行った処理を利用するプログラムを __init__
メソッド内に記述する必要がある場合は、それらの処理は super().__init__()
の後に記述する必要があります。
create_widgets
メソッドの定義
次に、GUI クラスの抽象メソッドをオーバーライドする、create_widgets
などのメソッドが行う処理を具体的に記述する必要があります。
ウィジェットがなければ Mbtree_GUI
の処理は行えないので、最初にGUI に表示する ウィジェットを作成する create_widgets
メソッドを定義 します。
前回の記事で、Mbtree_GUI では、下記の 4 つのボタンを利用することに決めたので、その 4 つのボタンのウィジェットを作成する必要 があります。また、ゲーム木は matplotlib の Figure に描画するので、Figure を作成する必要 もあります。
ボタンの表示と対応するカーソルキー | |
---|---|
親ノードへ移動 | ← |
一つ前の兄弟ノードへ移動 | ↑ |
一つ後の兄弟ノードへ移動 | ↓ |
子孫ノードの中の先頭のノードへ移動 | → |
ボタンのウィジェットの作成
まず、4 つのボタンのウィジェットを作成することにします。そのためには、それぞれのボタンのウィジェットを代入する Mbtree_GUI クラスの 属性の名前を決める必要 があり、本記事ではボタンに表示する矢印の方向から、下記のように命名しました。
ボタンの表示 | 属性の名前 |
---|---|
← | left_button |
↑ | up_button |
→ | right_button |
↓ | down_button |
ボタンのウィジェットは、基底クラスである GUI クラス の create_button
メソッドを使って作成できるので、create_widgets
は下記のプログラムのように記述できます。なお、本記事ではボタンの幅を 100 ピクセルに設定しましたが、自由に変更してもかまいません。
def create_widgets(self):
self.left_button = self.create_button("←", 100)
self.up_button = self.create_button("↑", 100)
self.right_button = self.create_button("→", 100)
self.botton_button = self.create_button("↓", 100)
Mbtree_GUI.create_widgets = create_widgets
Figure の大きさの計算
部分木を描画する Figure を作成するため には、Figure の大きさを決める必要 があります。どのようにしてその大きさを決めることができるかについて少し考えてみて下さい。
Mbtree_GUI では、draw_subtree
を使って、下図のような部分木を描画します。赤丸が、draw_subtree
で描画を行う際に仮引数 centernode
に代入したノードです。
draw_subtree
が描画する 部分木 は、draw_subtree
の仮引数 centernode
と maxdepth
に代入される値によって大きく変化 します。例えば、上図の部分木と、先程描画したルートノードを中心とする深さ 2 までの部分木では、縦幅も横幅も大きく異なります。
部分木の描画 は、Figure に登録された Axes に対して行います。そのため、Axes の表示範囲 を centernode
と maxdepth
にどのような値が代入された場合 でも、draw_subtree
が描画する 部分木がすべて収まるだけの大きさに設定する 必要があります。そのためには、部分木の縦幅と横幅の最大値 を調べる必要があります。
Axes の表示範囲が決まれば、Figure の大きさ を Axes の表示範囲の 縦幅と横幅と同じ比率で設定すればよい ことがわかる(そうしなければ、縦と横の縮尺が異なる、ゆがんだ部分木が描画されてしまいます)ので、Axes の表示範囲を考えることにします。
draw_subtree
が描画する部分木の大きさに合わせて、Figure のサイズを変更するという方法も考えられますが、Figure のサイズを変化させると、JupyterLab のセルの大きさが変化するため、見た目があまりよくないと思いましたので、本記事では Figure のサイズを Figure の作成後に変更する方法は採用しません。
まず、部分木の 横幅の最大値 を考えることにします。draw_subtree
では、深さ 0 のノードから、最大で深さ 9 までのノードを描画するので、最大で横に深さが 0 から 9 までの 10 個のゲーム盤が描画される ことになります。draw_subtree
では、ゲーム盤を 横方向に間を 2 だけ開けて描画する ので、最大で 5 * 10 = 50 の幅が必要 になることが分かります。
次に、部分木の 縦幅の最大値 を考えることにします。上図では、赤丸の centernode
から深さ 9 までの部分木を描画していますが、centernode
の深さが浅い場合に、深いノードまでの部分木を描画 した場合は、縦方向に描画しなければならない ノードの数が多くなりすぎる ことになります。例えば、以前の記事で説明したように、深さ 0 から深さ 9 までの ゲーム木全体を描画 すると、ゲーム木の高さが 100 万を超える ことになります。
部分木の高さ は、draw_subtree
で描画する 部分木の深さを浅くすることで減らす ことができます。centernode
の子ノードまでの部分木が、最も浅い部分木になるので、Mbtree_GUI では下図のように、centernode
の子ノードまでの部分木を描画 することにします。
次に、centernode
の子ノードまでを描画した場合の部分木の高さの最大値を考えることにします。どのように計算するかがわからない場合は、具体例を使って部分木の高さを計算する と良いでしょう。そこで、最初に上図の部分木の高さの計算方法について考えることにします。どのように計算すればよいかについて少し考えてみて下さい。
まず、centernode
とその子ノードを表す、下図の赤枠の中に、ゲーム盤が最大でいくつ縦に並ぶかを計算します。
その数は、centernode
の子ノードの数によって変化 します。centernode
の深さ(depth)を d とすると、深さ d のノードは d 手目の局面なので、合法手の数 は 9 - d になります。ただし、決着がついた局面には合法手が存在しないので、その数は下記のようになります。
-
centernode
の局面が、決着がついた局面 の場合は 子ノードは存在しない ので、赤枠の中にはcenternode
が 1 つだけ描画 される - 決着がついていない 場合は、赤枠の中に 子ノードが 9 - d 個縦に並んで描画 される
〇×ゲームでは、深さが 9 の局面 は必ず決着がついた局面になります。そのため、上記から、赤枠の中に並ぶゲーム盤の最大値 は以下のようになります。
- 深さ d が 9 の場合は 1
- それ以外の場合は 9 - d
次に下図の緑枠の中に、ゲーム盤が最大でいくつ縦に並ぶかを計算します。
図から、緑枠の高さ は、赤枠の高さ に、centernode
の 兄弟ノードの高さを加えたもの であることがわかります。ただし、深さが 0 のルートノードだけは 親ノードを持たない ので、兄弟ノードは存在せず、緑枠の高さは赤枠の高さと同じになる 点に注意して下さい。
d が 0 以外の場合に、深さ d のノードの 親ノードの深さは d - 1 なので、centernode
の親ノードは 9 - (d - 1) = 10 - d 個 の子ノードを持つことになります。centernode
は その中の 1 つ なので、centernode
の 兄弟ノードの数 は 10 - d - 1 = 9 - d で計算できます。
従って、上図の緑枠の中に縦に並ぶゲーム盤の数の最大値は、赤枠の中の最大値 + 兄弟ノードの数 になるので、以下のようになります。
d | 赤枠の最大値 | 兄弟ノードの数 | 最大値 |
---|---|---|---|
0 | 9 - 0 = 9 | 0 | 9 |
1 ~ 8 | 9 - d | 9 - d | 18 - 2 * d |
9 | 1 | 9 - 9 = 0 | 1 |
0
から 9
までのそれぞれの最大値を計算すると以下のようになるので、部分木では、最大で縦に 16 個 のゲーム盤が並べて描画されることが分かります。
d |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
最大値 | 9 | 16 | 14 | 12 | 10 | 8 | 6 | 4 | 2 | 1 |
draw_subtree
では、ゲーム盤を横方向に間を 1 だけ開けて描画するので、最大で 4 * 16 = 64 の高さが必要 になることが分かります。
従って、Axes の表示範囲 の x、y 座標をそれぞれ 0 ~ 50、と 0 ~ 64 に設定し、Figure の大きさを 横 50、縦 64 の比率で設定すればよい ことがわかりました。
この 50 と 64 という数値は、Figure の作成と Axes の表示範囲を設定する際に必要となるので、下記のプログラムの 5、6 行目のように、__init__
メソッド内で、width
と height
という 属性に代入する ことにします。
なお、前回の記事で説明したように、super
はクラスの定義の中で記述する必要がある ので、これまでのように、通常の関数として __init__
を定義し、Mbtree_GUI.__init__ = __init__
を実行して __init__
を 定義する事はできない点に注意 して下さい。
Mbtree_GUI というクラスを 定義し直した ので、先程修正した create_widgets
の定義 は、下記のプログラムの 9、10 行目の create_widgets
の定義で上書き されて 無効になる 点にも注意して下さい。ただし、create_widgets
は、この すぐ後で定義し直す ので、下記では何も行わない関数として create_widgets
を定義しました。
1 class Mbtree_GUI(GUI):
2 def __init__(self, mbtree, size=1):
3 self.mbtree = mbtree
4 self.size = size
5 self.width = 50
6 self.height = 64
7 super().__init__()
8
9 def create_widgets(self):
10 pass
元と同じなので省略
行番号のないプログラム
class Mbtree_GUI(GUI):
def __init__(self, mbtree, size=1):
self.mbtree = mbtree
self.size = size
self.width = 50
self.height = 64
super().__init__()
def create_widgets(self):
pass
def create_event_handler(self):
pass
def display_widgets(self):
pass
def update_gui(self):
pass
def update_widgets_status(self):
pass
修正箇所
class Mbtree_GUI(GUI):
def __init__(self, mbtree, size=1):
self.mbtree = mbtree
self.size = size
+ self.width = 50
+ self.height = 64
super().__init__()
元と同じなので省略
下記は、Figure を作成する処理を create_widgets
に加えた プログラムです。なお、Marubatsu_GUI では、create_widgets
の記述が長くなってしまうため、create_figure
というメソッドを別途定義して Figure を作成しましたが、Mbtree_GUI クラスの create_widgets
は記述が短いので create_widgets
の中にその処理を記述しました。
-
7 行目:以前の記事で説明したように、
%matplotlib widget
を実行した場合にdisplay
で Figure を描画する場合は、Figure が 2 回表示されないようにするために、matplotlib のインタラクティブモードを OFF にした状態で Figure を作成する必要があり、with plt.ioff():
のブロック内でで Figure を作成することでそれを行っている -
8、9 行目:
self.width
とself.height
に、Figure の大きさを調整するためのself.size
を乗算した大きさの Figure を作成する - 10 ~ 13 行目:以前の記事で説明した方法で、Figure に対して、タイトルやツールバーなどの表示が行われないようにする
1 def create_widgets(self):
2 self.left_button = self.create_button("←", 100)
3 self.up_button = self.create_button("↑", 100)
4 self.right_button = self.create_button("→", 100)
5 self.down_button = self.create_button("↓", 100)
6
7 with plt.ioff():
8 self.fig, self.ax = plt.subplots(figsize=[self.width * self.size,
9 self.height * self.size])
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_GUI.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
self.left_button = self.create_button("←", 100)
self.up_button = self.create_button("↑", 100)
self.right_button = self.create_button("→", 100)
self.down_button = self.create_button("↓", 100)
with plt.ioff():
self.fig, self.ax = plt.subplots(figsize=[self.width * self.size,
self.height * self.size])
self.fig.canvas.toolbar_visible = False
self.fig.canvas.header_visible = False
self.fig.canvas.footer_visible = False
self.fig.canvas.resizable = False
Mbtree_GUI.create_widgets = create_widgets
修正箇所
def create_widgets(self):
self.left_button = self.create_button("←", 100)
self.up_button = self.create_button("↑", 100)
self.right_button = self.create_button("→", 100)
self.down_button = self.create_button("↓", 100)
+ with plt.ioff():
+ self.fig, self.ax = plt.subplots(figsize=[self.width * self.size,
+ self.height * self.size])
+ self.fig.canvas.toolbar_visible = False
+ self.fig.canvas.header_visible = False
+ self.fig.canvas.footer_visible = False
+ self.fig.canvas.resizable = False
Mbtree_GUI.create_widgets = create_widgets
display_widgets
メソッドの定義
次に、create_widgets
で作成した ウィジェットを配置して表示 する display_widgets
メソッドを定義します。そのためには、4 つのボタンをどのように配置するかを決める必要があります。4 つのボタンをゲーム機のコントローラーの十字キーのように、上下左右の位置に配置することは可能ですが、一般的に、ボタンなどの GUI のレイアウト は、GUI を実装する際に 最初に厳密に決めなければならないものではありません。その理由は、一生懸命厳密なレイアウトを決めたとしても、後から GUI にボタンなどの機能を追加したりした場合に、せっかく厳密に決めたレイアウトを一から設計し直す必要が生じるからです。そのため、一般的には、厳密な GUI のレイアウトは、GUI の機能をすべて実装してから行うと良い でしょう。本記事では、とりあえず ←、→、↑、↓ の順で横に並べて配置することにします。また、ゲーム木を描画する Figure はその下に配置することにします。
下記は、そのように display_widgets
を記述したプログラムです。
- 4 行目:4 つのボタンを横に並べて配置した Hbox を作成する
-
5 行目:上記の Hbox と Figure を縦に並べた VBox を作成し、
display
で表示する
1 import ipywidgets as widgets
2
3 def display_widgets(self):
4 hbox = widgets.HBox([self.left_button, self.right_button, self.up_button, self.down_button])
5 display(widgets.VBox([hbox, self.fig.canvas]))
6
7 Mbtree_GUI.display_widgets = display_widgets
行番号のないプログラム
import ipywidgets as widgets
def display_widgets(self):
hbox = widgets.HBox([self.left_button, self.right_button, self.up_button, self.down_button])
display(widgets.VBox([hbox, self.fig.canvas]))
Mbtree_GUI.display_widgets = display_widgets
上記の修正後に、下記のプログラムを実行して、Mbtree_GUI クラスのインスタンスを作成すると、実行結果のように、上部に 4 つのボタンが表示され、その下に 横 50 縦 64 の非常に大きな Figure されるようになります。Figure が大きすぎるので、下記の実行結果には 4 つのボタンの部分だけを表記します。なお、作成したインスタンスは今回の記事では特に利用しませんが、mbtree_gui
という変数に代入することにします。
mbtree_gui = Mbtree_GUI(mbtree)
実行結果
Figure の大きさを調整するために、キーワード引数 size
に様々な値を記述して上記のプログラムを実行した所、下記のように、実引数に size=0.15
を指定して Mbtree_GUI のインスタンスを作成すると、ちょうど良い大きさの Figure が表示されるようになることがわかりました。そこで、後で __init__
メソッドの仮引数 size
をデフォルト値が 0.15
のデフォルト引数に修正することにします。他の値が良いと思った人は自由に変更して下さい。
mbtree_gui = Mbtree_GUI(mbtree, size=0.15)
実行結果
update_gui
メソッドの定義
次に、GUI の表示 を行う update_gui
を定義して、Figure に draw_subtree
を使って部分木を描画する ようにします。そのためには、部分木の描画を行う draw_subtree
を修正する必要があります。何を修正する必要があるかについて少し考えてみて下さい。
draw_subtree
の修正
Mbtree クラスの draw_subtree
メソッドは、下記のプログラムのように、その中で部分木を描画する Figure と Axes を作成 していますが、Mbtree_GUI では、部分木を create_widgets
で作成した Figure の Axes に対して描画を行う 必要があります。
def draw_subtree(self, centernode=None, size=0.25, lw=0.8, maxdepth=2):
略
fig, ax = plt.subplots(figsize=(width * size, height * size))
略
そこで、draw_subtree
に、デフォルト値を None
とする仮引数 ax
を追加し、ax
が None
の場合のみ draw_subtree
内で Figure を作成する ように修正することにします。なお、この修正は、以前の記事で draw_node
に対して行ったものと同じものです。
下記は、そのように draw_subtree
を修正したプログラムです。
-
1 行目:デフォルト値を
None
とするデフォルト引数ax
を追加する -
5 ~ 10 行目:
ax
がNone
の場合に、Figure の作成などの処理を行うようにする
1 def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
2 if parent is not None:
3 height += (len(parent.children) - 1) * 4
4 parent.height = height
5 if ax is None:
6 fig, ax = plt.subplots(figsize=(width * size, height * size))
7 ax.set_xlim(0, width)
8 ax.set_ylim(0, height)
9 ax.invert_yaxis()
10 ax.axis("off")
元と同じなので省略
11
12 Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
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
node.draw_node(ax=ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=dy)
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
sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
dy += sibling.height
dx = 5 * parent.depth
parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
node = parent
while node.parent is not None:
node = node.parent
node.height = height
dx = 5 * node.depth
node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
Mbtree.draw_subtree = draw_subtree
修正箇所
-def draw_subtree(self, centernode=None, size=0.25, lw=0.8, maxdepth=2):
+def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
if parent is not None:
height += (len(parent.children) - 1) * 4
parent.height = height
- 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")
+ 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")
元と同じなので省略
Mbtree.draw_subtree = draw_subtree
update_gui
の定義
最終的には Figure に描画する部分木を、ボタンで移動できるようにする必要がありますが、ボタンの イベントハンドラをまだ記述していない ので、現時点 では 常にルートノードを中心とした深さ 1 までの部分木を描画 することにします。下記は、そのように update_gui
を記述したプログラムです。
- 2 行目:それまでに Axes に描画されていた内容をクリアする。ax.clear() で、Axes の描画範囲や、上下の反転などの設定がリセットされるので、この後で設定し直す必要がある
- 3、4 行目:Axes の描画範囲を、先程計算した範囲に設定する
- 5、6 行目:Axes の y 軸の上下を反転させ、目盛りを表示しないようにする
-
7 行目:
draw_subtree
で、ルートノードを中心とする、深さ 1 までの部分木を描画する
1 def update_gui(self):
2 self.ax.clear()
3 self.ax.set_xlim(0, self.width)
4 self.ax.set_ylim(0, self.height)
5 self.ax.invert_yaxis()
6 self.ax.axis("off")
7 self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
8
9 Mbtree_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
self.ax.clear()
self.ax.set_xlim(0, self.width)
self.ax.set_ylim(0, self.height)
self.ax.invert_yaxis()
self.ax.axis("off")
self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
Mbtree_GUI.update_gui = update_gui
上記の修正後に、下記のプログラムを実行すると、実行結果のように部分木は描画されません。その理由を少し考えてみて下さい。
mbtree_gui = Mbtree_GUI(mbtree, size=0.15)
実行結果
部分木が描画されない理由と修正
部分木が描画されない理由は、Mbtree_GUI クラスの インスタンスを作成した際に実行 される __init__
メソッドの中で、draw_gui
を呼び出す処理を記述していない からです。
以前の記事で Marubatsu_GUI のインスタンスを作成した場合はゲーム盤が表示されるのに、Mbtree_GUI では描画されないのはおかしいと思った方がいるかもしれません。Marubatsu_GUI の場合は、以下のような理由でゲーム盤が描画されます。
- Marubatsu_GUI クラスのインスタンスは、Marubatsu クラスの
play
メソッドの中の下記の 4 行目部分で作成される。
1 def play(self, 略):
略
2 # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
3 if gui:
4 mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, seed=seed, size=size)
5 else:
6 mb_gui = None
7
8 self.restart()
9 return self.play_loop(mb_gui)
- その後で、上記の 9 行目で
play_loop
メソッドが呼び出される -
play_loop
の中の下記のプログラムの 6 行目で、update_gui
が呼び出される
1 def play_loop(self, mb_gui):
略
2 if verbose:
3 if gui:
4 # AI どうしの対戦の場合は画面を描画しない
5 if ai[0] is None or ai[1] is None:
6 mb_gui.update_gui()
略
つまり、Marubatsu_GUI の場合は、Marubatsu_GUI のインスタンスを作成した際にゲーム盤が描画されているわけではなく、作成後に Marubatsu クラスの play_loop
内で update_gui
が呼び出されてゲーム盤が描画されているということです。
そこで、下記のプログラムの 9 行目のように、GUI クラスの __init__
メソッドを、draw_widgets
を実行してウィジェットを配置した後で update_gui
を呼び出すようにすることで、GUI の表示を更新する処理を行うように修正することにします。
1 def __init__(self):
2 # %matplotlib widget のマジックコマンドを実行する
3 get_ipython().run_line_magic('matplotlib', 'widget')
4
5 self.disable_shortcutkeys()
6 self.create_widgets()
7 self.create_event_handler()
8 self.display_widgets()
9 self.update_gui()
10
11 GUI.__init__ = __init__
行番号のないプログラム
def __init__(self):
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
self.disable_shortcutkeys()
self.create_widgets()
self.create_event_handler()
self.display_widgets()
self.update_gui()
GUI.__init__ = __init__
修正箇所
def __init__(self):
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
self.disable_shortcutkeys()
self.create_widgets()
self.create_event_handler()
self.display_widgets()
+ self.update_gui()
GUI.__init__ = __init__
上記の修正後に、下記のプログラムを実行すると、実行結果のようにルートノードを中心とした部分木が描画されるようになります。
mbtree_gui = Mbtree_GUI(mbtree, size=0.15)
Axes の配置位置の調整
ところで、上記の実行結果で 部分木の描画位置 が、4 つのボタンより かなり下に表示 されている点が気になる人はいないでしょうか。これは、Figure の中に作成された Axes の配置の位置が原因 ですが、上図では、Figure、Axes の背景色が白色なので、Figure と Axes の範囲が見た目からはわかりません。
Axes の配置位置の確認
そこで、Figure の中で、Axes がどのような位置に配置されているかを確認する ために、下記のプログラムのように Figure の背景色を灰色にし、Axes の軸の目盛りを表示するように update_gui
を修正してみることにします。なお、Figure の背景色の変更は、2 行目のように、set_facecolor
というメソッドで行うことができます。
- 2 行目:Figure の背景色を薄い灰色(lightgray)にする
- 7 行目:軸の目盛りを表示しないようにする処理をコメントにして実行しないようにする。この部分は一時的に実行しないようにするだけなので、コメントにした
1 def update_gui(self):
2 self.fig.set_facecolor("lightgray")
3 self.ax.clear()
4 self.ax.set_xlim(0, self.width)
5 self.ax.set_ylim(0, self.height)
6 self.ax.invert_yaxis()
7 # self.ax.axis("off")
8
9 self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
10
11 Mbtree_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
self.fig.set_facecolor("lightgray")
self.ax.clear()
self.ax.set_xlim(0, self.width)
self.ax.set_ylim(0, self.height)
self.ax.invert_yaxis()
# self.ax.axis("off")
self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
+ self.fig.set_facecolor("lightgray")
self.ax.clear()
self.ax.set_xlim(0, self.width)
self.ax.set_ylim(0, self.height)
self.ax.invert_yaxis()
- self.ax.axis("off")
+# self.ax.axis("off")
self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
Mbtree_GUI.update_gui = update_gui
set_facecolor
の詳細については、下記のリンク先を参照して下さい。
上記の修正後に、下記のプログラムを実行すると、実行結果のような画像が描画されます。
mbtree_gui = Mbtree_GUI(mbtree, size=0.15)
実行結果
上図の 灰色の部分が Figure を、内部の 枠の中の白い部分が Axes を表しており、Figure は 4 つのボタンのすぐ下に配置されていることが分かります。従って、部分木が 4 つのボタンよりかなり下に描画されるのは、Axes が Figure のかなり内側に配置されている ことが原因であることがわかります。
%matplotlib widget
による Axes の配置位置の変化
筆者が試行錯誤して調べてみた所、%matplotlib widget
を実行するか どうかで subplots
メソッドで作成した Axes が Figure の内部のどこに配置されるかが若干異なる ことがわかりました。%matplotlib inline
を実行すると、%matplotlib widget
を実行する前の状態に戻すことができるので、そのことは下記の 2 つのプログラムで確認することができます。
下記は、%matplotlib widget
を実行しない状態で、subplots
メソッドで Figure と Axes を作成して表示するプログラムです。
%matplotlib inline
plt.close("all")
fig, ax = plt.subplots(facecolor="lightgray")
実行結果
上記の 2 行目の意味は説明しないとわからないと思いますので説明します。下記に関する内容は以前の記事でも説明したので、忘れた方はそちらも見て下さい。
%matplotlib widget
を実行しない 場合は、JupyterLab のセルを実行すると、そのセルで作成された Figure は 自動的に描画された後で閉じられて利用できなくなります。
一方、%matplotlib widget
を実行した後 では、JupyterLab のセルを実行しても、作成した Figure は 自動的に閉じられず、残り続ける ことになります。これは、Figure を閉じてしまうと、後から Figure の内容を変更できなくなるからです。
今回の記事では、Mbtree_GUI クラスを作成する際に、%matplotlib widget
が実行され、多くの Figure を描画しました。そのため、それらの Figure が残り続けています。その後で、上記のプログラムのように、%matplotlib inline
を実行して JupyterLab のセルを実行すると、それまでに作成されたFigure が全て描画されてしまいます。2 行目の plt.close("all")
は、それまでに作成した Figure を全て閉じるという処理を行います。そうすることで、セルの実行後にこれまでに作成した 余計な Figure が描画されなくなります。
plt.close
の詳細については、下記のリンク先を参照して下さい。
下記は、%matplotlib widget
を実行した状態で、subplots
メソッドで Figure と Axes を作成した場合のプログラムです。
%matplotlib widget
fig, ax = plt.subplots(facecolor="lightgray")
実行結果
下記は、%matplotlib widget
を実行しない場合と、実行した場合で表示される Figure を並べたものです。比べてみるとわかるように、%matplotlib widget
を実行した場合 のほうが、Axes が Figure のより内側に作成される ようです。このようなことが起きる原因については筆者の調べた限りではよくわかりませんでした1が、この問題は、Axes を plt.subplots
ではない、別の方法で作成する ことで解決することができます。
add_axes
メソッドによる Axes の配置位置の設定
plt.subplots
は 1 つの Axes が配置された Figure を作成する場合だけでなく、(本記事ではまだそのような処理を行ったことはありませんが)複数の Axes2 が規則正しく並べて配置される ような Figure を作成する際で便利なので良く使われますが、Figure の中での Axes の配置 をどのような大きさで、どのような位置に行うかを 厳密に指定して作成することはできません。そのような場合は、Figure の add_axes
というメソッドを利用する必要があります。add_axes
の 実引数 には、Figure の中に配置する Axes の 位置と大きさ を (left, bottom, width, height)
という 4 つの要素を持つ tuple で記述 します。
ぞれぞれの要素の意味は以下の通りです。
意味 | |
---|---|
left |
Figure の左端を 0、右端を 1 とする座標 |
bottom |
Figure の下端を 0、上端を 1 とする座標 |
width |
Figure の幅を 1 とした場合の Axes の幅 |
height |
Figure の高さを 1 とした場合の Axes の高さ |
plt.axes((left, bottom, width, height))
で、current figure に対して同様の方法で Axes を追加することができます。
add_axes
は、Figure のメソッド なので、利用する場合は、下記のプログラムのように、plt.figure()
で Figure だけを作成した後 で、その Figure に対して add_axes
を呼び出します。下記は、Figure の下半分と、右上に Axes を作成するプログラムです。
fig = plt.figure()
ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.4]) # 左下が (0.1, 0.1)、幅が 0.8、高さが 0.4 の Axes
ax2 = fig.add_axes([0.6, 0.6, 0.3, 0.3]) # 左下が (0.6, 0.3)、幅が 0.3、高さが 0.3 の Axes
上記のプログラムでは行っていませんが、ax1
と ax2
を使って、2 つの Axes にそれぞれ別の画像を描画することができます。
Figure の add_axes
メソッドの詳細については、下記のリンク先を参照して下さい。
plt.axes()
の詳細については、下記のリンク先を参照して下さい。
create_widgets
の修正
下記は、Axes を Figure 全体に配置するように create_widgets
を修正するプログラムです。
-
5 行目:Figure の左下の (0, 0) を基準に、幅 1、高さ 1 の範囲を表すように
add_axes
を記述することで、Figure 全体に配置するように Axes を作成する
1 def create_widgets(self):
元と同じなので省略
2 with plt.ioff():
3 self.fig = plt.figure(figsize=[self.width * self.size,
4 self.height * self.size])
5 self.ax = self.fig.add_axes([0, 0, 1, 1])
元と同じなので省略
6
7 Mbtree_GUI.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
self.left_button = self.create_button("←", 100)
self.up_button = self.create_button("↑", 100)
self.right_button = self.create_button("→", 100)
self.down_button = self.create_button("↓", 100)
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_GUI.create_widgets = create_widgets
修正箇所
def create_widgets(self):
元と同じなので省略
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])
元と同じなので省略
Mbtree_GUI.create_widgets = create_widgets
また、下記のプログラムのように、update_gui
を元に戻します。
def update_gui(self):
self.ax.clear()
self.ax.set_xlim(0, self.width)
self.ax.set_ylim(0, self.height)
self.ax.invert_yaxis()
self.ax.axis("off")
self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
- self.fig.set_facecolor("lightgray")
self.ax.clear()
self.ax.set_xlim(0, self.width)
self.ax.set_ylim(0, self.height)
self.ax.invert_yaxis()
-# self.ax.axis("off")
+ self.ax.axis("off")
self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
Mbtree_GUI.update_gui = update_gui
上記の修正後に、下記のプログラムを実行すると、実行結果のように部分木が 4 つのボタンのすぐ下に描画されるようになります。
mbtree_gui = Mbtree_GUI(mbtree, size=0.15)
実行結果
実は、Marubatsu_GUI クラスが作成する ゲーム盤を表示する Figure も、Axes が Figure のかなり内側に配置 されています。ただし、Marubatsu_GUI の場合は、ゲーム盤の上と下にメッセージを表示する必要がある ので、Axes が Figure のかなり内側に描画されても問題はありません。ただし、Marubatsu_GUI を定義した際に Axes が Figure のかなり内側に描画されることを筆者は知らなかったので、それは筆者が意図したものではなく、偶然そのようになっていたに過ぎません。
選択中のノードを表す属性の追加と update_gui
の修正
Mbtree_GUI では、ボタンによって描画する部分木の 中心となるノードを変更 しますが、そのためには、どのノードが中心となるノードであるか を表す情報を 記録する必要 があります。そこで、本記事では centernode
という属性にその情報を代入する ことにします。
次に、centernode
属性の初期化の処理 を __init__
メソッド内で記述する必要があります。初期設定 では centernode
に ルートノードを設定するのが自然 だと思いますので、本記事では下記のプログラムのように __init__
メソッドを修正することにします。なお、ついでに 仮引数 size
を デフォルト値を 0.15 とするデフォルト引数に変更 しました。
また、update_gui
を、centernode
属性を中心 とし、centernode
の 次の深さまで を描画するように修正しました。なお、ゲーム木の深さは 9 までしかない ので、maxdepth
が 9 を超えないように しないとエラーが発生してしまう可能性が生じる点に注意して下さい。
なお、super
が記述された __init__
メソッドを変更するので、下記のプログラムのように、Mbtree_GUI クラスを定義しなおしました。
-
7 行目:
centernode
属性にルートノードを代入して初期化する -
18 行目:
centernode
の深さ + 1 を計算してmaxdepth
に代入する。ただし、max
を使って深さが 9 を超えないようにする -
19 行目:
centernode
を中心とする、深さmaxdepth
までの部分木を描画する
1 class Mbtree_GUI(GUI):
2 def __init__(self, mbtree, size=0.15):
3 self.mbtree = mbtree
4 self.size = size
5 self.width = 50
6 self.height = 64
7 self.centernode = self.mbtree.root
8 super().__init__()
9
元と同じなので省略
10
11 def update_gui(self):
12 self.ax.clear()
13 self.ax.set_xlim(0, self.width)
14 self.ax.set_ylim(0, self.height)
15 self.ax.invert_yaxis()
16 self.ax.axis("off")
17
18 maxdepth = min(self.centernode.depth + 1, 9)
19 self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)
20
元と同じなので省略
行番号のないプログラム
class Mbtree_GUI(GUI):
def __init__(self, mbtree, size=0.15):
self.mbtree = mbtree
self.size = size
self.width = 50
self.height = 64
self.centernode = self.mbtree.root
super().__init__()
def create_widgets(self):
self.left_button = self.create_button("←", 100)
self.up_button = self.create_button("↑", 100)
self.right_button = self.create_button("→", 100)
self.down_button = self.create_button("↓", 100)
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):
pass
def display_widgets(self):
hbox = widgets.HBox([self.left_button, self.right_button, self.up_button, self.down_button])
display(widgets.VBox([hbox, self.fig.canvas]))
def update_gui(self):
self.ax.clear()
self.ax.set_xlim(0, self.width)
self.ax.set_ylim(0, self.height)
self.ax.invert_yaxis()
self.ax.axis("off")
maxdepth = min(self.centernode.depth + 1, 9)
self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)
def update_widgets_status(self):
pass
修正箇所
class Mbtree_GUI(GUI):
def __init__(self, mbtree, size=0.15):
self.mbtree = mbtree
self.size = size
self.width = 50
self.height = 64
+ self.centernode = self.mbtree.root
super().__init__()
元と同じなので省略
def update_gui(self):
self.ax.clear()
self.ax.set_xlim(0, self.width)
self.ax.set_ylim(0, self.height)
self.ax.invert_yaxis()
self.ax.axis("off")
+ maxdepth = min(self.centernode.depth + 1, 9)
- self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
+ self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
実行結果は先ほどと同じなので省略しますが、上記の修正後に、下記のプログラムを実行すると、ルートノードを中心とした、深さ 1 までの部分木が表示されます。
mbtree_gui = Mbtree_GUI(mbtree)
create_event_handler
の定義
create_event_handler
には、以下の内容を記述します。下の 2 つは、Marubatsu_GUI の create_event_handler
と同様の方法で記述できます。
- 4 つのボタンをクリックした際に呼び出されるイベントハンドラの定義
- 4 つのボタンとイベントハンドラの結び付け
- 4 つのボタンに対応するキーを押した時に対応するイベントハンドラを呼び出す処理
4 つのボタンに対する処理を、順番に記述する事にします。
親ノードへの移動
← ボタンを押した時に行う、部分木の中心となる centernode
属性のノードを 親のノードへ移動 する処理は、以下の手順で行うことができます。最後の Figure の描画の更新の処理の記述を忘れないように注意 して下さい。
-
centernode
に親ノードが存在する場合は、親ノードをcenternode
に代入する - 親ノードが存在しない場合は何もしない
-
update_gui
を呼び出して、Figure の描画を更新する
従って、このイベントハンドラは下記のプログラムで記述できます。
イベントハンドラの名前は、Marubatsu_GUI のボタンのイベントハンドラと同様に、ボタンのウィジェットを代入する属性の名前の前後に on_
と _clicked
を加えたものにしました。仮引数 b=None
は、イベントループから呼び出される場合と、キー入力で呼び出される場合の両方に対応できるようにするためのものです。詳細は以前の記事を参照して下さい。
def on_left_button_clicked(b=None):
if self.centernode.parent is not None:
self.centernode = self.centernode.parent
self.update_gui()
子ノードへの移動
→ ボタンを押した時に行う子のノードへの移動は、以下の手順で行うことができます。
-
centernode
に子ノードが存在する場合は、最初の子ノードをcenternode
に代入する - 子ノードが存在しない場合は何もしない
-
update_gui
を呼び出して、Figure の描画を更新する
従って、このイベントハンドラは下記のプログラムで記述できます。子ノードが存在するか どうかは、親ノードの children
属性の要素の数が 0 より大きいか どうかで判定できます。
def on_right_button_clicked(b=None):
if len(self.centernode.children) > 0:
self.centernode = self.centernode.children[0]
self.update_gui()
親ノードと子ノードへの移動に対応する create_event_handler
の定義
まだ、兄弟ノードへの移動に対応するイベントハンドラを定義していませんが、親ノードと子ノードへの移動ができれば、Mbtree_GUI の部分木の表示を変更できるようになるので、親ノードと子ノードへの移動に対応する create_event_handler
を、下記のプログラムのように定義する事にします。
プログラムは、新しい機能を実装した際に、なるべくその都度正しく実装できているかどうかをチェックするべきです。そうしないと、バグが発生した場合に、どこでバグが発生したかを見つけることが困難になるからです。
- 2 ~ 10 行目:上記のイベントハンドラを定義する
- 12、13 行目:イベントハンドラとボタンを結び付ける
- 15 ~ 24 行目:キー入力とイベントハンドラを結び付ける。詳細は以前の記事を参照
1 def create_event_handler(self):
2 def on_left_button_clicked(b=None):
3 if self.centernode.parent is not None:
4 self.centernode = self.centernode.parent
5 self.update_gui()
6
7 def on_right_button_clicked(b=None):
8 if len(self.centernode.children) > 0:
9 self.centernode = self.centernode.children[0]
10 self.update_gui()
11
12 self.left_button.on_click(on_left_button_clicked)
13 self.right_button.on_click(on_right_button_clicked)
14
15 def on_key_press(event):
16 keymap = {
17 "left": on_left_button_clicked,
18 "right": on_right_button_clicked,
19 }
20 if event.key in keymap:
21 keymap[event.key]()
22
23 # fig の画像イベントハンドラを結び付ける
24 self.fig.canvas.mpl_connect("key_press_event", on_key_press)
25
26 Mbtree_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
def on_left_button_clicked(b=None):
if self.centernode.parent is not None:
self.centernode = self.centernode.parent
self.update_gui()
def on_right_button_clicked(b=None):
if len(self.centernode.children) > 0:
self.centernode = self.centernode.children[0]
self.update_gui()
self.left_button.on_click(on_left_button_clicked)
self.right_button.on_click(on_right_button_clicked)
def on_key_press(event):
keymap = {
"left": on_left_button_clicked,
"right": on_right_button_clicked,
}
if event.key in keymap:
keymap[event.key]()
# fig の画像イベントハンドラを結び付ける
self.fig.canvas.mpl_connect("key_press_event", on_key_press)
Mbtree_GUI.create_event_handler = create_event_handler
上記の修正後に、下記のプログラムを実行し、← と → ボタンまたは、左右のカーソルキーを押すと、部分木の中心となるノードが変化するようになることを確認して下さい。
mbtree_gui = Mbtree_GUI(mbtree)
なお、→ ボタンを何度も押すと、下記の画面が表示され、それ以上 → ボタンを押しても表示が変化しないようになります。その理由について少し考えてみて下さい。
その理由は、上図では、右上の 〇 が勝利した局面を中心とする部分木が描画されているからです。決着がついた局面には子ノードは存在しないので、右上のゲーム盤の右には何も表示されず、→ ボタンをクリックしても子ノードには移動しません。
現状の Mbtree_GUI には、このように、中心となるノードが何であるかがわかりづらいという欠点 があります。その欠点については次回の記事で修正します。
兄弟ノードへの移動に対応する create_event_handler
の定義
↑ ボタンを押した時に行う、一つ前の兄弟ノードへの移動の処理は、親ノードや子ノードへの移動のように簡単ではありません。その方法について少し考えてみて下さい。
一つ前の兄弟ノードへの移動の処理は、以下の手順で行うことができます。
-
centernode
に親ノードが存在しない場合は、兄弟ノードは存在しないので何もしない - 親ノードの子ノードの一覧を表す
centernode.parent.childnode
属性に代入された list の中で、centernode
が何番のインデックスに代入されているかを調べる - そのインデックスが 0 の場合は、一つ前の兄弟ノードは存在しないので何もしない
- 0 以外の場合は、一つ前のインデックスの要素を
childnode
に代入する -
update_gui
を呼び出して、Figure の描画を更新する
上記の処理を行うイベントハンドラは、下記のプログラムで記述できます。
- 2 行目:親ノードが存在する場合のみ処理を行うようにする
-
3 行目:親ノードの子ノードの list の中で、
centernode
が代入されている要素のインデックスを、以前の記事で説明したindex
メソッドを使って計算する -
4、5 行目:
index
が 0 以上の場合は、一つ前のインデックスをcenternode
とする
1 def on_up_button_clicked(b=None):
2 if self.centernode.parent is not None:
3 index = self.centernode.parent.children.index(self.centernode)
4 if index > 0:
5 self.centernode = self.centernode.parent.children[index - 1]
6 self.update_gui()
同様に、一つ後の兄弟ノードへの移動は、上記の手順 3、4 を下記のように修正することで行うことができます。
3. そのインデックスが最後のインデックスの場合は、一つ後の兄弟ノードは存在しないので何もしない
4. それ以外の場合は、一つ後のインデックスの要素を childnode
に代入する
上記の処理を行うイベントハンドラは、下記のプログラムで記述できます。
-
4 行目:
centernode
が、親ノードの子ノードの最後の要素でないことは、list 最後の要素を表す [-1] とis not
演算子を利用することで判定できる
1 def on_down_button_clicked(b=None):
2 if self.centernode.parent is not None:
3 index = self.centernode.parent.children.index(self.centernode)
4 if self.centernode.parent.children[-1] is not self.centernode:
5 self.centernode = self.centernode.parent.children[index + 1]
6 self.update_gui()
最後の要素に代入されているということは、その要素のインデックスが list の要素の数 - 1 であるということです。従って、上記の 4 行目は、下記のように親ノードの子ノードの数を使った式で記述することもできます。
if index != len(self.centernode.parent.children) - 1:
下記は、上記のイベントハンドラを組み込んだ create_event_handler
のプログラムです。先程とほぼ同様なので、説明は省略します。
def create_event_handler(self):
def on_left_button_clicked(b=None):
if self.centernode.parent is not None:
self.centernode = self.centernode.parent
self.update_gui()
def on_right_button_clicked(b=None):
if len(self.centernode.children) > 0:
self.centernode = self.centernode.children[0]
self.update_gui()
def on_up_button_clicked(b=None):
if self.centernode.parent is not None:
index = self.centernode.parent.children.index(self.centernode)
if index > 0:
self.centernode = self.centernode.parent.children[index - 1]
self.update_gui()
def on_down_button_clicked(b=None):
if self.centernode.parent is not None:
index = self.centernode.parent.children.index(self.centernode)
if self.centernode.parent.children[-1] is not self.centernode:
self.centernode = self.centernode.parent.children[index + 1]
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)
def on_key_press(event):
keymap = {
"left": 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]()
# fig の画像イベントハンドラを結び付ける
self.fig.canvas.mpl_connect("key_press_event", on_key_press)
Mbtree_GUI.create_event_handler = create_event_handler
上記の修正後に、下記のプログラムを実行し、←、→、↑、↓ ボタンまたは、カーソルキーを押すと、部分木の中心となるノードが変化するようになることを確認して下さい。
mbtree_gui = Mbtree_GUI(mbtree)
また、下図のように、最も縦幅が必要になる、深さ 1 のノードを中心とする部分木が、Figure の中にすべて表示されることも確認して下さい。
今回の記事のまとめ
今回の記事では、ゲーム木を表示する GUI を作成し、4 つのボタンで描画する部分木の中心となるノードを移動できるようにしました。ただし、現状では中心となるノードが何であるかが分かりにくいなどの問題があります。次回の記事ではそれらの問題を修正するので、どのような問題があるかについて考えておいてください。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した tree.py です。
以下のリンクは、今回の記事で更新した gui.py です。
次回の記事