目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
draw_node
のバグの修正
前回の記事で修正した draw_node
にはいくつかのバグがある事がわかりましたので、最初にその修正を行うことにします。
深さが 9 以外の部分木を描画した際のバグ
1 つ目のバグは、draw_tree
で 深さが 9 以外 の部分木を描画すると、エラーが発生したり、最も深いノードのエッジの描画がおかしくなる場合があるというものです。下記は、ルートノードから深さ 1 までの部分木を描画するプログラムで、実行結果のような エラーが発生 します。また、その際に、実行結果のような画像が描画されます。
from tree import Mbtree
mbtree = Mbtree()
mbtree.draw_tree(maxdepth=1)
実行結果
略
File c:\Users\ys\ai\marubatsu\091\tree.py:92, in Node.draw_node(self, ax, size, lw, dx, dy)
90 else:
91 if len(self.children) > 0:
---> 92 edgeheight = self.height - self.children[-1].height
94 # 自分自身のノードを (dx, dy) に描画する
95 Marubatsu_GUI.draw_board(ax, self.mb, lw=lw, dx=dx, dy=dy)
AttributeError: 'Node' object has no attribute 'height'
エラーの原因の考察
エラーメッセージの in Node.draw_node
から draw_node
の中でエラーが発生 したことがわかります。また、描画される画像から ルートノードとそのエッジは正しく描画される ので、問題は 深さ 1 のノード を draw_node
で 描画した際に発生 したことが推測できます。
下記は、draw_node
の定義の一部でです。
1 def draw_node(self, ax=None, size=0.25, lw=0.8, dx=0, dy=0):
2 width = 8
3 if ax is None:
略
4 else:
5 if len(self.children) > 0:
6 edgeheight = self.height - self.children[-1].height
略
先程の、エラーメッセージから、上記の 6 行目で AttributeError: 'Node' object has no attribute 'height' というエラーが発生していることがわかります。このエラーメッセージから、6 行目の self
または self.children[-1]
のいずれかに height
という属性が存在しないことがわかります。
そこで、下記のプログラムで深さ 1 のノードとその子ノードの height
属性を表示してみると、実行結果から、深さ 1 のノードの height
属性には 4
が代入されていますが、深さ 2 のノードには height
属性が存在しない ことが確認できます。
print(mbtree.root.children[0].height)
print(mbtree.root.children[0].children[0].height)
実行結果
4
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[2], line 2
1 print(mbtree.root.children[0].height)
----> 2 print(mbtree.root.children[0].children[0].height)
AttributeError: 'Node' object has no attribute 'height'
そこで、深さ 2 のノードの height
属性が存在しない理由について考察することにします。
ノードの height
属性は、下記の draw_tree
の 4 行目で calc_node_height
を呼び出すことで、深さが 0 ~ maxdepth
までのノード に対して計算が行われます。先ほどのプログラムでは maxdepth
に 1
が代入 されていたので、深さが 0 と 1 のノード に対して高さが計算され、 height
属性にその値が代入されます。
1 def draw_tree(self, startnode=None, size=0.25, lw=0.8, maxdepth=2):
2 if startnode is None:
3 startnode = self.root
4 self.calc_node_height(maxdepth)
略
先程エラーが発生した際の self
は、深さ 1 のノードなので、self.children
は深さ 2 のノード を表しますが、上記で考察したように、height
属性が計算されたノードは深さ 0 と 1 のノードなので、深さ 2 のノードには height
属性が存在しません。これが、AttributeError: 'Node' object has no attribute 'height' というエラーが発生した原因です。
なお、前回の記事で下記のプログラムを記述した際にエラーが発生しなかった原因は、描画する最も深いノードが、子ノードが絶対に存在しない深さ 9 のノード だったからです。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][0], maxdepth=9)
深さが 9 のノードを draw_node
で描画する場合は、下記の 1 行目の条件式が False
になるので、エラーが発生する 2 行目のプログラムが実行されることはありません。
if len(self.children) > 0:
edgeheight = self.height - self.children[-1].height
エッジの描画がおかしくなる例
上記ではエラーが発生しましたが、エラーが発生しない場合もあり、その場合は エッジの描画がおかしくなります。
例えば、下記のプログラムを実行して 深さが 6 のノードから深さ 9 までの部分木を描画した後 で、先程と同じプログラムで深さ 2 までの部分木を描画すると、実行結果のように、深さが 1 のノードから子ノードへのエッジがおかしな描画が行われます。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][0], maxdepth=9)
mbtree.draw_tree(maxdepth=1)
実行結果(深さ 6 のノードからの画像は前回の記事と同じなので省略します)
このようなことが起きる原因は、以下の通りです。
-
mbtree.draw_tree(mbtree.nodelist_by_depth[6][0], maxdepth=9)
によって、すべてのノードの高さが計算 され、height
属性にその値が代入される -
mbtree.draw_tree(maxdepth=1)
によって、深さが 0 と 1 のノードの高さが計算 されてheight
属性に代入されるが、それ以外の深さのノードに対しては何の処理も行われない - 従って、深さが 2 以上のノードの
height
属性 には、mbtree.draw_tree(mbtree.nodelist_by_depth[6][0], maxdepth=9)
によって計算されたノードの高さの値が 代入されたまま である
上記は、下記のプログラムで深さ 1 と 2 のノードの高さを表示することで確認できます。実行結果から、深さ 2 のノードの高さが 14672 という非常に高い値になっていることが確認できます。これは mbtree.draw_tree(mbtree.nodelist_by_depth[6][0], maxdepth=9)
によって計算された、ゲーム木全体を描画した場合 の深さが 2 のノードの高さを表します。
print(mbtree.root.children[0].height)
print(mbtree.root.children[0].children[0].height)
実行結果
4
14672
問題の修正
先程のエラーは、下記のプログラムで 最も深いノードから延びるエッジの高さ を計算する処理で発生します。また、もう一つの問題はエッジの描画がおかしくなるというものです。
edgeheight = self.height - self.children[-1].height
しかし、よく考えてみると、そもそも最も深いノードから延びるエッジを 描画する必要はありません。従って、この問題は、draw_tree
で指定した maxdepth
の深さのノードを draw_node
で描画する際 に、上記の計算を行わず、エッジを描画しないようにする という方法で解決できます。そこで、下記のプログラムのように draw_node
に仮引数 maxdepth
を追加し、その深さのノードの場合はエッジを描画しないように修正することにします。
-
5 行目:互換性を考慮して、デフォルト値を
None
とする仮引数maxdepth
を追加する -
7、13 行目:子ノードが存在し、ノードの深さが
maxdepth
と等しくない場合にエッジの長さの計算と、エッジと子ノードの描画を行うように修正する
1 from marubatsu import Marubatsu_GUI
2 from tree import Node
3 import matplotlib.pyplot as plt
4
5 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
6 else:
7 if len(self.children) > 0 and maxdepth != self.depth:
8 edgeheight = self.height - self.children[-1].height
9
10 # 自分自身のノードを (dx, dy) に描画する
11 Marubatsu_GUI.draw_board(ax, self.mb, lw=lw, dx=dx, dy=dy)
12 # 子ノードが存在する場合に、エッジの線と子ノードを描画する
13 if len(self.children) > 0 and maxdepth != self.depth:
元と同じなので省略
14
15 Node.draw_node = draw_node
行番号のないプログラム
from marubatsu import Marubatsu_GUI
from tree import Node
import matplotlib.pyplot as plt
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
if len(self.children) > 0:
edgeheight = height - 4
else:
if len(self.children) > 0 and maxdepth != self.depth:
edgeheight = self.height - self.children[-1].height
# 自分自身のノードを (dx, dy) に描画する
Marubatsu_GUI.draw_board(ax, self.mb, lw=lw, dx=dx, dy=dy)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0 and maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
for childnode in self.children:
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
Node.draw_node = draw_node
修正箇所
from marubatsu import Marubatsu_GUI
from tree import Node
import matplotlib.pyplot as plt
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
else:
- if len(self.children) > 0:
+ if len(self.children) > 0 and maxdepth != self.depth:
edgeheight = self.height - self.children[-1].height
# 自分自身のノードを (dx, dy) に描画する
Marubatsu_GUI.draw_board(ax, self.mb, lw=lw, dx=dx, dy=dy)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
- if len(self.children) > 0:
+ if len(self.children) > 0 and maxdepth != self.depth:
元と同じなので省略
Node.draw_node = draw_node
次に、下記のプログラムの 10 行目のように、draw_tree
内で draw_node
を呼び出す際に、キーワード引数 maxdepth=maxdepth
を記述して、maxdepth
の深さのノードのエッジが表示されないように修正します。
1 def draw_tree(self, startnode=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
2 while len(nodelist) > 0 and depth <= maxdepth:
3 dy = 0
4 childnodelist = []
5 for node in nodelist:
6 if node is None:
7 dy += 4
8 childnodelist.append(None)
9 else:
10 node.draw_node(ax=ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
11
12 Mbtree.draw_tree = draw_tree
行番号のないプログラム
def draw_tree(self, startnode=None, size=0.25, lw=0.8, maxdepth=2):
if startnode is None:
startnode = self.root
self.calc_node_height(maxdepth)
width = 5 * (maxdepth - startnode.depth + 1)
height = startnode.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]
depth = startnode.depth
dx = 0
while len(nodelist) > 0 and depth <= maxdepth:
dy = 0
childnodelist = []
for node in nodelist:
if node is None:
dy += 4
childnodelist.append(None)
else:
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)
dx += 5
depth += 1
nodelist = childnodelist
Mbtree.draw_tree = draw_tree
修正箇所
def draw_tree(self, startnode=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
while len(nodelist) > 0 and depth <= maxdepth:
dy = 0
childnodelist = []
for node in nodelist:
if node is None:
dy += 4
childnodelist.append(None)
else:
- node.draw_node(ax=ax, size=size, lw=lw, dx=dx, dy=dy)
+ node.draw_node(ax=ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
Mbtree.draw_tree = draw_tree
上記の修正後に、先程と同じ下記のプログラムを実行することで、実行結果のようにバグが修正されたことが確認できます。
mbtree.draw_tree(maxdepth=1)
実行結果
ノードと子ノードの関係だけを描画した場合のバグ
前回の記事で draw_node
で子ノードを描画しないように修正しましたが、そのせいで下記のプログラムのように、キーワード引数 ax
を記述せずに draw_node
を呼び出して、そのノードと子ノードの関係だけを描画しようとした際に、子ノードが描画されなくなる という問題が発生します。
mbtree.root.draw_node()
実行結果
間違った修正方法
この問題を解決するためには、キーワード引数 ax
を記述せずに draw_node
が呼び出された場合は、子ノードを描画するように draw_node
を修正する必要があります。
そこで、下記のプログラムのように draw_node
を修正すれば良いと思った人がいるかもしれませんが、下記のプログラムは正しく動作しません。
-
6 行目:
ax
がNone
の場合のみ、子ノードを描画するように修正する
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
2 # 子ノードが存在する場合に、エッジの線と子ノードを描画する
3 if len(self.children) > 0 and maxdepth != self.depth:
4 plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
5 for childnode in self.children:
6 if ax is None:
7 Marubatsu_GUI.draw_board(ax, childnode.mb, dx=dx+5, dy=dy, lw=lw)
8 plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
9 dy += childnode.height
10
11 Node.draw_node = draw_node
行番号のないプログラム
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
if len(self.children) > 0:
edgeheight = height - 4
else:
if len(self.children) > 0 and maxdepth != self.depth:
edgeheight = self.height - self.children[-1].height
# 自分自身のノードを (dx, dy) に描画する
Marubatsu_GUI.draw_board(ax, self.mb, lw=lw, dx=dx, dy=dy)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0 and maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
for childnode in self.children:
if ax is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
Node.draw_node = draw_node
修正箇所
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0 and maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
for childnode in self.children:
+ if ax is None:
+ Marubatsu_GUI.draw_board(ax, childnode.mb, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
Node.draw_node = draw_node
実行結果は先ほどと同じなので省略しますが、上記の修正後に、先程と同じ下記のプログラムを実行しても、子ノードは描画されません。子ノードが描画されない原因について少し考えてみて下さい。
mbtree.root.draw_node()
正しい修正方法
子ノードが描画されない理由は、ax
に None
が代入されていた場合に、下記のプログラムの 5 行目で ax
に plt.subplots
で作成した Axes が代入されてしまうからです。そのため、先程の if ax is None
を実行した時点では、ax
の値は None
ではなくなっています。
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
2 width = 8
3 if ax is None:
4 height = len(self.children) * 4
5 fig, ax = plt.subplots(figsize=(width * size, height * size))
略
この問題を解決する一つの方法は、関数が呼び出された時 に仮引数 ax
に代入されていた値を 別の変数に代入して取っておく というものです。
他の方法としては、仮引数 maxdepth
の性質に注目する という方法があります。maxdepth
は draw_tree
で部分木を描画する際に、最も深いノードのエッジと子ノードを描画しないようにするためのものです。ノードと子ノードの関係だけを描画 する場合は、maxdepth
の情報は必要がない ので、キーワード引数 maxdepth
を記述せずに draw_node
を呼び出す ことになります。従って、下記のプログラムのように、maxdepth
が None
の場合に子ノードを描画する ようにすれば、この問題を解決することができます。本記事ではこの方法を採用しますが、わかりづらいと思った方はもう一つの方法を採用して下さい。
-
6 行目:
maxdepth
がNone
の場合のみ、子ノードを描画するように修正する
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
2 # 子ノードが存在する場合に、エッジの線と子ノードを描画する
3 if len(self.children) > 0 and maxdepth != self.depth:
4 plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
5 for childnode in self.children:
6 if maxdepth is None:
7 Marubatsu_GUI.draw_board(ax, childnode.mb, dx=dx+5, dy=dy, lw=lw)
8 plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
9 dy += childnode.height
10
11 Node.draw_node = draw_node
行番号のないプログラム
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
if len(self.children) > 0:
edgeheight = height - 4
else:
if len(self.children) > 0 and maxdepth != self.depth:
edgeheight = self.height - self.children[-1].height
# 自分自身のノードを (dx, dy) に描画する
Marubatsu_GUI.draw_board(ax, self.mb, lw=lw, dx=dx, dy=dy)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0 and maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
Node.draw_node = draw_node
修正箇所
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0 and maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
for childnode in self.children:
- if ax is None:
+ if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
Node.draw_node = draw_node
上記の修正後に、先程と同じ下記のプログラムを実行することで、実行結果のようにバグが修正されたことが確認できます。
mbtree.root.draw_node()
実行結果
draw_node
の改良
現状の draw_node
にはいくつか改良の余地があるので、改良することにします。どのような改良の余地があるかについて少し考えてみて下さい。
なお、本記事で紹介する以外の改良方法を思いついた方はぜひ実装してみて下さい。
決着がついたノードの区別
現状では、ゲーム木の局面の画像を見ても 決着がついているかどうかの区別がわかりづらい 図になっています。そこで、決着がついた局面の 勝敗結果 を下記のように描画して 一目でわかるようにする ことにします。色を変更したい人は自由に変更して下さい。
ゲーム盤の背景色 | |
---|---|
ゲーム中 | 白(これまでと同じ) |
〇 の勝利 | 水色 |
× の勝利 | 薄いピンク |
引き分け | 薄い黄色 |
ゲーム盤の背景色の変更方法
ゲーム盤の背景色を変更するためには、ゲーム盤を描画する位置に、ゲーム盤と同じ大きさ の、背景色で塗りつぶされた正方形を描画 します。その後で枠やマークを描画 することで、ゲーム盤の背景色を変更することができます。
上記の正方形の描画は、枠やマークを描画する前に行う必要がある 点に注意して下さい。枠やマークの描画の後で上記の正方形を描画すると、枠やマークの上に重ねて正方形が描画されるため、枠やマークが見えなくなってしまうからです。
matplotlib で正方形を描画するためには、patches モジュールで定義された、長方形を表す Rectangle
という Artist を作成する関数を利用します。
下記は Rectangle
の仮引数 です。
仮引数 | 意味 |
---|---|
xy | 長方形の左上の頂点の座標 (x, y) を表すシーケンス型 |
width | 長方形の幅 |
height | 長方形の高さ |
Rectangle
の詳細については、下記のリンク先を参照して下さい。
draw_board
の修正
ゲーム盤の描画は draw_board
で行うので、draw_board
を上記の表のようなゲーム盤を描画するように修正することにします。ただし、互換性を考慮し、True
が代入されていた場合のみゲーム盤の背景色を変えて結果(result)を表示(show)する show_result
という仮引数を追加 することにします。下記はそのように draw_board
を修正したプログラムです。
-
5 行目:デフォルト値を
False
とする、デフォルト引数show_result
を追加する -
7 ~ 18 行目:
show_result
がTrue
の場合のみゲーム盤の背景色を変更する。この処理は、枠やマークの描画の前に行う必要がある -
8 ~ 14 行目:
status
属性の値に応じた背景色をbgcolor
に代入する -
16 行目:ゲーム盤と同じ位置と大きさで、
bgcolor
で塗りつぶした正方形をRectangle
で作成する。塗りつぶしの色などの設定方法については以前の記事を参照すること -
17 行目:作成した正方形の Artist を
add_artist
で Axes に登録して描画する
1 from marubatsu import Marubatsu
2 import matplotlib.patches as patches
3
4 @staticmethod
5 def draw_board(ax, mb, show_result=False, dx=0, dy=0, lw=2):
6 # 結果によってゲーム盤の背景色を変更する
7 if show_result:
8 if mb.status == Marubatsu.PLAYING:
9 bgcolor = "white"
10 elif mb.status == Marubatsu.CIRCLE:
11 bgcolor = "lightcyan"
12 elif mb.status == Marubatsu.CROSS:
13 bgcolor = "lavenderblush"
14 else:
15 bgcolor = "lightyellow"
16 rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
17 height=mb.BOARD_SIZE, fc=bgcolor)
18 ax.add_patch(rect)
元と同じなので省略
19
20 Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
from marubatsu import Marubatsu
import matplotlib.patches as patches
@staticmethod
def draw_board(ax, mb, show_result=False, dx=0, dy=0, lw=2):
# 結果によってゲーム盤の背景色を変更する
if show_result:
if mb.status == Marubatsu.PLAYING:
bgcolor = "white"
elif mb.status == Marubatsu.CIRCLE:
bgcolor = "lightcyan"
elif mb.status == Marubatsu.CROSS:
bgcolor = "lavenderblush"
else:
bgcolor = "lightyellow"
rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
height=mb.BOARD_SIZE, fc=bgcolor)
ax.add_patch(rect)
# ゲーム盤の枠を描画する
for i in range(1, mb.BOARD_SIZE):
ax.plot([dx, dx + mb.BOARD_SIZE], [dy + i, dy + i], c="k", lw=lw) # 横方向の枠線
ax.plot([dx + i, dx + i], [dy, dy + mb.BOARD_SIZE], c="k", lw=lw) # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(mb.BOARD_SIZE):
for x in range(mb.BOARD_SIZE):
color = "red" if (x, y) == mb.last_move else "black"
Marubatsu_GUI.draw_mark(ax, dx + x, dy + y, mb.board[x][y], color, lw=lw)
Marubatsu_GUI.draw_board = draw_board
修正箇所
from marubatsu import Marubatsu
import matplotlib.patches as patches
@staticmethod
def draw_board(ax, mb, show_result=False, dx=0, dy=0, lw=2):
# 結果によってゲーム盤の背景色を変更する
+ if show_result:
+ if mb.status == Marubatsu.PLAYING:
+ bgcolor = "white"
+ elif mb.status == Marubatsu.CIRCLE:
+ bgcolor = "lightcyan"
+ elif mb.status == Marubatsu.CROSS:
+ bgcolor = "lavenderblush"
+ else:
+ bgcolor = "lightyellow"
+ rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
+ height=mb.BOARD_SIZE, fc=bgcolor)
+ ax.add_patch(rect)
元と同じなので省略
Marubatsu_GUI.draw_board = draw_board
次に、下記のプログラムの 3、9 行目のように draw_node
の中で、draw_board
を呼び出す処理に、キーワード引数 show_result=True
を追加します。
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
2 # 自分自身のノードを (dx, dy) に描画する
3 Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=dy)
4 # 子ノードが存在する場合に、エッジの線と子ノードを描画する
5 if len(self.children) > 0 and maxdepth != self.depth:
6 plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
7 for childnode in self.children:
8 if maxdepth is None:
9 Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
10 plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
11 dy += childnode.height
12
13 Node.draw_node = draw_node
行番号のないプログラム
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
if len(self.children) > 0:
edgeheight = height - 4
else:
if len(self.children) > 0 and maxdepth != self.depth:
edgeheight = self.height - self.children[-1].height
# 自分自身のノードを (dx, dy) に描画する
Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=dy)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0 and maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
Node.draw_node = draw_node
修正箇所
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
# 自分自身のノードを (dx, dy) に描画する
- Marubatsu_GUI.draw_board(ax, self.mb, lw=lw, dx=dx, dy=dy)
+ Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=dy)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0 and maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
for childnode in self.children:
if maxdepth is None:
- Marubatsu_GUI.draw_board(ax, childnode.mb, dx=dx+5, dy=dy, lw=lw)
+ Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
Node.draw_node = draw_node
上記の修正後に、下記のプログラムを実行することで、実行結果のように決着がついた局面の背景色が変化するようになります。なお、下記は 〇 の勝利、× の勝利、引き分けの全ての局面が含まれる部分木を、様々な部分木を表示するという試行錯誤で見つけました。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][100], maxdepth=9)
実行結果
部分木の最も深いノードに子ノードが存在する場合の表示
今回の記事の冒頭で、draw_tree
で表示する部分木の、最も深いノードのエッジと子ノードを描画しないように修正しました。例えば、下記のプログラムで 深さが 8 までの部分木を描画 すると、実行結果のようなゲーム木が描画されます。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][100], maxdepth=8)
先程決着のついた局面の背景色に色を塗るようにしたので、最も深い 深さが 8 のノード に 子ノードが存在するか どうかは、背景色が白いかどうか で判別することができますが、直観的ではないので わかりやすいとはいえない でしょう。
そこで、最も深いノードに子ノードが存在する場合は、子ノードが存在することを表す横棒のエッジを描画する ように工夫することにします。
下記は、そのように draw_node
を修正したプログラムです。
- 5 行目:子ノードが存在するかどうかだけを判定するように修正する
-
6 ~ 12 行目:ノードの深さが
maxdepth
と等しくない場合は、これまでと同じ方法で、エッジと子ノードを描画する -
13、14 行目:ノードの深さが
maxdepth
と等しい場合は、子ノードが存在することを表す、横に 1 本だけのエッジを描画する
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
2 # 自分自身のノードを (dx, dy) に描画する
3 Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=dy)
4 # 子ノードが存在する場合に、エッジの線と子ノードを描画する
5 if len(self.children) > 0:
6 if maxdepth != self.depth:
7 plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
8 for childnode in self.children:
9 if maxdepth is None:
10 Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
11 plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
12 dy += childnode.height
13 else:
14 plt.plot([dx + 3.5, dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
15
16 Node.draw_node = draw_node
行番号のないプログラム
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
if len(self.children) > 0:
edgeheight = height - 4
else:
if len(self.children) > 0 and maxdepth != self.depth:
edgeheight = self.height - self.children[-1].height
# 自分自身のノードを (dx, dy) に描画する
Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=dy)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0:
if maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
else:
plt.plot([dx + 3.5, dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
Node.draw_node = draw_node
修正箇所(if maxdepth != self.depth のインデントの修正は省略します)
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
# 自分自身のノードを (dx, dy) に描画する
Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=dy)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
- if len(self.children) > 0 and maxdepth != self.depth:
+ if len(self.children) > 0:
+ if maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
+ else:
+ plt.plot([dx + 3.5, dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
Node.draw_node = draw_node
上記の修正後に、下記のプログラムを実行することで、実行結果のように、最も深いノードに子ノードが存在する場合は、横棒のエッジが表示されるようになります。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][100], maxdepth=8)
実行結果
本記事では子ノードの数に関わらず、横棒を 1 本だけ描画しますが、子ノードの数を明確にしたい場合は、以下のような工夫を行うと良いでしょう。
- 横棒の数を変える
- 横棒の太さを変える
- 横棒の右に子ノードの数を数字で表示する
バランスの良い位置へのノードの描画
現状では、親ノード は、その右の 子ノード一覧の上端に描画 されますが、子ノードの一覧の真ん中に描画 したほうが、見た目のバランスが良いゲーム木 になります。具体的には、左下図を右下図のように描画するということです。そのためには、どのようにノードやエッジの描画位置を計算すればよいかについて少し考えてみて下さい。
ノードの描画位置の計算方法
まず、ルートノードの描画位置を計算する方法を考えることにします。ルートノードは、下図の 赤線の長さの分だけ下にずらして描画 する必要があります。
ルートノードは、Figure の上下のちょうど真ん中に描画する必要があるので、図の 2 本ある赤線の長さは等しくなります。また、Figure の高さはルートノードの height
属性に代入されているので、上図から赤線の長さは下記の式で計算することができることがわかります。
(ノードの height
属性 - 3) / 2
ノードの height
属性は、子ノードの高さの合計 を表すので、下図のように、ルートノード以外の場合 も、上記の式と全く同じ式 で赤線の長さを計算することができます。
下記は、上記の式を使って、ノードを描画する座標をずらして表示するように draw_node
を修正したプログラムです。
-
3 行目:ノードを描画する y 座標を先程の式を使って計算する。なお、
dy
はこの後でエッジなどを描画する際に必要になる のでy
という変数に計算した値を代入した - 4 行目:上記で計算した座標にノードを描画する
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
2 # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
3 y = dy + (self.height - 3) / 2
4 Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=y)
元と同じなので省略
5
6 Node.draw_node = draw_node
行番号のないプログラム
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
if len(self.children) > 0:
edgeheight = height - 4
else:
if len(self.children) > 0 and maxdepth != self.depth:
edgeheight = self.height - self.children[-1].height
# 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
y = dy + (self.height - 3) / 2
Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=y)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0:
if maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
else:
plt.plot([dx + 3.5, dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
Node.draw_node = draw_node
修正箇所
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
# 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
+ y = dy + (self.height - 3) / 2
- Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=dy)
+ Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=y)
元と同じなので省略
Node.draw_node = draw_node
上記の修正後に、下記のプログラムを実行すると、実行結果のように ノードがバランスの良い位置に表示される ようになります。ただし、エッジの描画の処理は変更していないので、エッジが変な位置に描画されるという問題があります。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][100], maxdepth=8)
実行結果
エッジの描画の手順の変更
これまでは、エッジは下記の手順で描画を行いました。
- 下図左の緑の折れ線を描画する
- それぞれの子ノードに対して横線を描画する
ただし、ノードをバランスの良い位置に描画した場合は上図右のようになるため、上図左の緑の線を 一本の折れ線で描画することはできません。
また、ノードをバランスの良い位置に描画した場合の エッジの縦線の上端と下端の座標 をあらかじめ 計算するのは少し面倒 なので、エッジの描画方法を下記のように変えることにします。なお、下図の赤と青の線は、真ん中の子ノードに対して描画するエッジです。
- 下図の 緑の横線 を描画する
- それぞれの子ノードに対して、下図の 赤の横線 を描画する
- それぞれの子ノードに対して、上にノードがあれば 下図の 青の縦線 を描画する
緑の線に対応するエッジの描画
緑の線に対応するエッジは、ノードの描画位置である (dx, y) を基準として、(3.5 + dx, 1.5 + y) から (4 + dx, 1.5 + y) までを plot
で描画できます。ノードの描画位置の y 座標が dy
から y
に変化 した点と、縦棒を描画しなくなった点を除けば、元のプログラムと同じです。
下記は、上図の緑の線に対応するエッジを描画するように修正するプログラムです。わかりやすいように、線の太さを 3 にし、実際に緑の線で描画するようにしました。線の太さと色は、エッジが正しく描画されたことが確認できた後で元に戻すことにします。なお、エッジの縦棒を描画する際に利用していた edgeheight
は必要がなくなったので削除しました。
-
4 行目の下 にあった
edgeheight
を計算する処理を削除する - 8 行目:緑の線に対応するエッジを描画する。その際に、縦棒を描画しないようにする
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
2 for childnode in self.children:
3 childnode.height = 4
4 # この下にあった edgeheight を計算する処理を削除する
元と同じなので省略
5 # 子ノードが存在する場合に、エッジの線と子ノードを描画する
6 if len(self.children) > 0:
7 if maxdepth != self.depth:
8 plt.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="g", lw=3)
元と同じなので省略
9
10 Node.draw_node = draw_node
行番号のないプログラム
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
# 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
y = dy + (self.height - 3) / 2
Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=y)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0:
if maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="g", lw=3)
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
else:
plt.plot([dx + 3.5, dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
Node.draw_node = draw_node
修正箇所
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
for childnode in self.children:
childnode.height = 4
- if len(self.children) > 0:
- edgeheight = height - 4
- else:
- if len(self.children) > 0:
- edgeheight = self.height - self.children[-1].height元と同じなので省略
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0:
if maxdepth != self.depth:
- plt.plot([dx + 3.5, dx + 4, dx + 4], [dy + 1.5, dy + 1.5, dy + 1.5 + edgeheight], c="k", lw=lw)
+ plt.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="g", lw=3)
元と同じなので省略
Node.draw_node = draw_node
上記の修正後に、下記のプログラムを実行すると、実行結果のように子ノードが存在するノードの右に緑の線が描画され、エッジの縦棒が描画されなくなります。ただし、最も深いノードの子ノードのエッジが少し上にずれてしまう という問題が発生しています。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][100], maxdepth=8)
実行結果
最も深いノードのエッジの描画の修正
最も深いノードのエッジは、黒色で描画されていることからわかるように、上記で修正したプログラムではなく、下記のプログラムの 3 行目で描画しています。下記の 3 行目の処理を実行すると、上記のように横線がずれてしまう理由について少し考えてみて下さい。
1 if maxdepth != self.depth:
略
2 else:
3 plt.plot([dx + 3.5, dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
上記の 3 行目のプログラムは修正していないので、横線がずれる理由 は、横線の左の ノードが描画される位置が変化したため です。ノードの描画位置は、(height 属性 - 3) / 2 という式で計算しますが、最も深いノードの height
属性 には、下記の calc_node_height
の 4、5 行目の処理によって 4
が代入されます。
1 def calc_node_height(self, maxdepth:int):
2 for depth in range(maxdepth, -1, -1):
3 for node in self.nodelist_by_depth[depth]:
4 if depth == maxdepth:
5 node.height = 4
6 else:
7 node.calc_height()
従って、(height 属性 - 3) / 2 の計算結果は (4 - 3) / 2 = 0.5 になる ため、最も深いノード は、先程の修正が行われる前と比べて 0.5 だけ下にずれて描画が行われます。一方で、最も深いノードのエッジの描画位置は修正していない ので、先程の図のように、一見すると、エッジのほうが上にずれて描画されるように見えることになります。
従って、この問題は下記のプログラムの 4 行目のように、最も深いノードのエッジを描画する際に、dy
ではなく、ノードの描画位置を表す y
を基準に描画する ことで解決することができます。
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
2 if maxdepth != self.depth:
元と同じなので省略
3 else:
4 plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
5
6 Node.draw_node = draw_node
行番号のないプログラム
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
# 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
y = dy + (self.height - 3) / 2
Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=y)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0:
if maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="g", lw=3)
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
dy += childnode.height
else:
plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
Node.draw_node = draw_node
修正箇所
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
if maxdepth != self.depth:
元と同じなので省略
else:
- plt.plot([dx + 3.5, dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
+ plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
Node.draw_node = draw_node
上記の修正後に、下記のプログラムを実行すると、実行結果のように、最も深いノードのエッジが正しい位置に描画されるようになります。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][100], maxdepth=8)
実行結果
赤い線に対応するエッジの描画
赤い線に対応するエッジは、それぞれの子ノードが描画される位置に応じて描画する必要があり、子ノードは dy
に対して (子ノードの height 属性 - 3) / 2 だけ下にずらした位置に描画します。従って、下記のプログラムのように修正することで、赤い線に対応するエッジを正しい位置に描画することができるようになります。なお、わかりやすさを重視して、先程と同様に太さが 3 で赤い線で描画するようにしました。
- 5、6 行目:子ノードへのエッジを描画する座標を計算して描画する
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
2 for childnode in self.children:
3 if maxdepth is None:
4 Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
5 edgey = dy + (childnode.height - 3) / 2 + 1.5
6 plt.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="r", lw=3)
7 dy += childnode.height
元と同じなので省略
8
9 Node.draw_node = draw_node
行番号のないプログラム
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
# 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
y = dy + (self.height - 3) / 2
Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=y)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0:
if maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="g", lw=3)
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
edgey = dy + (childnode.height - 3) / 2 + 1.5
plt.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="r", lw=3)
dy += childnode.height
else:
plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
Node.draw_node = draw_node
修正箇所
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
- plt.plot([dx + 4 , dx + 4.5], [dy + 1.5, dy + 1.5], c="k", lw=lw)
+ edgey = dy + (childnode.height - 3) / 2 + 1.5
+ plt.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="r", lw=3)
dy += childnode.height
元と同じなので省略
Node.draw_node = draw_node
上記の修正後に、下記のプログラムを実行すると、実行結果のように、子ノードへの赤い横線のエッジが描画されるようになります。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][100], maxdepth=8)
実行結果
青い線に対応するエッジの描画
青い線に対応するエッジは、一つ前 の子ノードへの 横線の y 座標から、現在 の子ノードへの 横線の y 座標まで の 縦線を引く ことで描画することができます。
従って、一つ前 の子ノードへの 横線の y 座標を記録しておく 必要があります。また、最初の子ノードには一つ前の子ノードが存在しない ので、青い線を描画する必要がない 点にも注意が必要です。どのようなプログラムで青い線に対応するエッジを描画できるかについて少し考えてみて下さい。
下記のような処理を行うことで青い線に対応するエッジを描画することができます。
- 一つ前(previous)の子ノードへの横線の y 座標を
prevy
という変数に代入する - 最初の子ノードには一つ前の子ノードが存在しないので、
prevy
をNone
で初期化 する - 子ノードの赤い線のエッジを
edgey
の高さに描画した後で、prevy
にNone
が代入されていない場合にprevy
からedgey
までの縦線を描画する - その後で、
prevy
にedgey
を代入して更新 する
下記はそのように draw_node
を修正したプログラムです。
-
4 行目:子ノードに対する繰り返し処理を行う前に、
prevy
をNone
で初期化する -
10、11 行目:
prevy
がNone
ではない場合に、(dx + 4, prevy) から (dx + 4, edgey) まで縦線を描画する -
12 行目:
prevy
にedgey
を代入する
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
2 if maxdepth != self.depth:
3 plt.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="g", lw=3)
4 prevy = None
5 for childnode in self.children:
6 if maxdepth is None:
7 Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
8 edgey = dy + (childnode.height - 3) / 2 + 1.5
9 plt.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="r", lw=3)
10 if prevy is not None:
11 plt.plot([dx + 4 , dx + 4], [prevy, edgey], c="b", lw=3)
12 prevy = edgey
13 dy += childnode.height
14 else:
15 plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
16
17 Node.draw_node = draw_node
行番号のないプログラム
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
# 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
y = dy + (self.height - 3) / 2
Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=y)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0:
if maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="g", lw=3)
prevy = None
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
edgey = dy + (childnode.height - 3) / 2 + 1.5
plt.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="r", lw=3)
if prevy is not None:
plt.plot([dx + 4 , dx + 4], [prevy, edgey], c="b", lw=3)
prevy = edgey
dy += childnode.height
else:
plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
Node.draw_node = draw_node
修正箇所
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
if maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="g", lw=3)
+ prevy = None
for childnode in self.children:
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
edgey = dy + (childnode.height - 3) / 2 + 1.5
plt.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="r", lw=3)
+ if prevy is not None:
+ plt.plot([dx + 4 , dx + 4], [prevy, edgey], c="b", lw=3)
+ prevy = edgey
dy += childnode.height
else:
plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
Node.draw_node = draw_node
上記の修正後に、下記のプログラムを実行すると、実行結果のように、青い縦線のエッジが描画されるようになります。これでバランスの良いノードの描画の処理の実装は完了です。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][100], maxdepth=8)
実行結果
ノードと子ノードの関係だけを描画した場合のバグ
今回の記事の最初で、キーワード引数 ax
を記述せずに draw_node
を呼び出した場合にバグがあることを示しました。実は、ノードをバランスよく表示するように修正した結果、その時と同様に、下記のプログラムを実行するとおかしな表示が行われます。
mbtree.root.draw_node()
実行結果
おかしな点は、以下の通りです。
- ルートノードの局面とその右の緑色のエッジが描画されない
- 子ノードが少し上にずれて描画される
上記のような描画が行われる原因と修正方法について少し考えてみて下さい。
ルートノードが描画されない原因の検証
ルートノードは draw_node
の下記の部分で計算されますが、キーワード引数 ax
を記述せずに draw_node
を呼び出した場合は、ノードの高さの計算を行っていない ため、下記の self.height
には、直前の draw_tree
で計算したノードの高さ が代入されています1。
y = dy + (self.height - 3) / 2
Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=y)
そのことは、下記のプログラムでルートノードの高さを表示すると、100 万以上の数値が表示されることから確認できます。従って、ルートノードが描画されなかった理由 は、(self.height - 3) / 2
が約 50 万になるため、Axes の 表示範囲の外 である、y 座標が約 50 万の位置に ルートノードが描画されたから です。
print(mbtree.root.height)
実行結果
1020672
これは、先程下記のプログラムを実行した結果、draw_tree
の中で、深さ 8 までのゲーム木を描画した際の各ノードの高さが計算され、その時のルートノードの高さが mbtree.root.height
に代入されたままになっているからです。
mbtree.draw_tree(mbtree.nodelist_by_depth[6][100], maxdepth=8)
子ノードの描画位置がずれる問題の検証
子ノードの描画は下記のプログラムの 3 行目で行われますが、先程子ノードへのエッジの描画位置を変更したにも関わらず、子ノードの描画位置をそれにあわせて修正していません。そのため、子ノードの描画位置がずれて表示されてしまいます。
1 for childnode in self.children:
2 if maxdepth is None:
3 Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
バグの修正
上記から、下記のプログラムのように draw_node
を修正することで、バグを修正することができます。なお、下記では、エッジの色と太さを元に戻す修正も行いましたが、その修正の説明と修正箇所への反映は省略します。
-
5 行目:
ax
がNone
だった場合は、ノードの高さは 4 行目で計算してローカル変数height
に代入済なので、height
属性にその高さを代入する2 -
7 行目:子ノードを描画する際の
y
座標を計算してchildnodey
に代入する -
9 行目:y 座標が
childnodey
の位置に子ノードを描画するように修正する -
10 行目:子ノードへのエッジの y 座標を
childnodey
から計算してedgey
に代入する
1 def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
2 width = 8
3 if ax is None:
4 height = len(self.children) * 4
元と同じなので省略
5 self.height = height
元と同じなので省略
6 for childnode in self.children:
7 childnodey = dy + (childnode.height - 3) / 2
8 if maxdepth is None:
9 Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=childnodey, lw=lw)
10 edgey = childnodey + 1.5
元と同じなので省略
11
12 Node.draw_node = draw_node
行番号のないプログラム
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
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")
for childnode in self.children:
childnode.height = 4
self.height = height
# 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
y = dy + (self.height - 3) / 2
Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, lw=lw, dx=dx, dy=y)
# 子ノードが存在する場合に、エッジの線と子ノードを描画する
if len(self.children) > 0:
if maxdepth != self.depth:
plt.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="k", lw=lw)
prevy = None
for childnode in self.children:
childnodey = dy + (childnode.height - 3) / 2
if maxdepth is None:
Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=childnodey, lw=lw)
edgey = childnodey + 1.5
plt.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="k", lw=lw)
if prevy is not None:
plt.plot([dx + 4 , dx + 4], [prevy, edgey], c="k", lw=lw)
prevy = edgey
dy += childnode.height
else:
plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
Node.draw_node = draw_node
修正箇所
def draw_node(self, ax=None, maxdepth=None, size=0.25, lw=0.8, dx=0, dy=0):
width = 8
if ax is None:
height = len(self.children) * 4
元と同じなので省略
+ self.height = height
元と同じなので省略
for childnode in self.children:
+ childnodey = dy + (childnode.height - 3) / 2
if maxdepth is None:
- Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=dy, lw=lw)
+ Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=childnodey, lw=lw)
- edgey = dy + (childnode.height - 3) / 2 + 1.5
+ edgey = childnodey + 1.5
元と同じなので省略
Node.draw_node = draw_node
上記の修正後に、下記のプログラムを実行すると、実行結果のように、正しい描画が行われるようになることが確認できます。
mbtree.root.draw_node()
実行結果
今回の記事のまとめ
今回の記事では、ゲーム木の視覚化の処理のバグの修正と、表示の改良を行いました。次回の記事ではゲーム木全体を把握するための視覚化の工夫について紹介する予定です。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
以下のリンクは、今回の記事で更新した tree.py です。
次回の記事