0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その91 ゲーム木の視覚化の処理のバグの修正と改良

Last updated at Posted at 2024-06-20

目次と前回の記事

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

ルールベースの 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 までのノード に対して計算が行われます。先ほどのプログラムでは maxdepth1 が代入 されていたので、深さが 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 行目axNone の場合のみ、子ノードを描画するように修正する
 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()

正しい修正方法

子ノードが描画されない理由は、axNone が代入されていた場合に、下記のプログラムの 5 行目で axplt.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 の性質に注目する という方法があります。maxdepthdraw_tree で部分木を描画する際に、最も深いノードのエッジと子ノードを描画しないようにするためのものです。ノードと子ノードの関係だけを描画 する場合は、maxdepth の情報は必要がない ので、キーワード引数 maxdepth を記述せずに draw_node を呼び出す ことになります。従って、下記のプログラムのように、maxdepthNone の場合に子ノードを描画する ようにすれば、この問題を解決することができます。本記事ではこの方法を採用しますが、わかりづらいと思った方はもう一つの方法を採用して下さい。

  • 6 行目maxdepthNone の場合のみ、子ノードを描画するように修正する
 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_resultTrue の場合のみゲーム盤の背景色を変更する。この処理は、枠やマークの描画の前に行う必要がある
  • 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 という変数に代入する
  • 最初の子ノードには一つ前の子ノードが存在しないので、prevyNone で初期化 する
  • 子ノードの赤い線のエッジを edgey の高さに描画した後で、prevyNone が代入されていない場合に prevy から edgey までの縦線を描画する
  • その後で、prevyedgey を代入して更新 する

下記はそのように draw_node を修正したプログラムです。

  • 4 行目:子ノードに対する繰り返し処理を行う前に、prevyNone で初期化する
  • 10、11 行目prevyNone ではない場合に、(dx + 4, prevy) から (dx + 4, edgey) まで縦線を描画する
  • 12 行目prevyedgey を代入する
 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 行目axNone だった場合は、ノードの高さは 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 です。

次回の記事

  1. 一度も draw_tree を実行していない場合は、height 属性に値が代入されていないため、今回の記事の冒頭で発生したエラーと同じエラーが発生します

  2. プログラム内の全てのローカル変数 heightself.height に修正するという方法も考えられますが、プログラムの修正箇所が多くなるので本記事では採用しませんでした

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?