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を一から作成する その139 Mbtree_Anim クラスのバグの修正と改良

Last updated at Posted at 2024-12-15

目次と前回の記事

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

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

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
test.py テストに関する関数
util.py ユーティリティ関数の定義。現在は gui_play のみ定義されている
tree.py ゲーム木に関する Node、Mbtree クラスの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

Mbtree_Anim のバグの修正と改良

前回の記事で αβ 法による評価値の計算を紹介しましたが、αβ 法 が行う処理の手順は 直観的にわかりづらい と思った人が多いのではないかと思います。そこで、以前の記事で Mbtree_Anim クラスを作成して幅優先アルゴリズムや深さ優先アルゴリズムで ゲーム木が作成される手順評価値が計算される手順アニメーションで視覚化 したように、αβ 法によって評価値が計算される手順も Mbtree_Anim で視覚化できるようにすることにします。

その前に、Mbtree_Anim にバグがあることが判明したので、今回の記事ではその修正を行うことにします。また、Mbtree_Anim の表示の改良を行うことにします。

Mbtree_Anim クラスで新たに発生したバグ

下記は、以前の記事dfscore.mbtree に保存した、深さ優先アルゴリズムで評価値を計算した ゲーム木の評価値の計算過程を Mbtree_Anim で表示 するプログラムです1

from tree import Mbtree, Mbtree_Anim

mbtree = Mbtree.load("../data/dfscore")
Mbtree_Anim(mbtree, isscore=True)

実行結果

実行結果から、以下の問題がある事がかわかります。

  • 一番上 の局面の 評価値の表示 が、Figure の外に はみ出して見えなくなっている
  • 評価値の 文字が大きすぎて、上部が一つ上の局面に 重なっている

バグの原因の検証

この問題は、以前の記事で dfscore.mbtree を Mbtree_Anim で表示した際には、局面の上に 評価値を表示していなかった ため、発生していませんでした

その後の記事 で Mbtree クラスの draw_subtree メソッドを修正して 局面の上に評価値を表示する ように修正した時点でこの バグが発生していた のですが、その際に Mbtree_Anim での表示 が正しく行われることを 確認していなかった ため、今回の記事まで そのようなバグがあることが 判明していませんでした

一番上の局面の評価値の 表示がはみ出る問題 は、以前の記事 で同じ問題に 対処した方法と同じ方法 で修正することができます。

評価値の文字の表示 は Mbtree クラスの draw_subtree メソッドで行っており、評価値の 文字の大きさは仮引数 size に代入された値 で決まります。現状の Mbtree_Anim クラスは update_gui メソッド内で draw_subtree メソッドを呼び出す際 に、仮引数 size に対応する 実引数を記述していないため、適切な大きさで評価値が表示されません2。そのため、この問題は size=self.size を実引数に記述して draw_subtree メソッドを呼び出すように修正することで解決することができます。

Mbtree_Anim クラスの __init__ メソッドの修正

まず、Mbtree_Anim クラスの __init__ メソッドを下記のプログラムのように修正します。この修正について忘れた方は、以前の記事を復習してください。

  • 6 行目:局面の上に評価値を表示するようになったので、その分だけ表示の高さを表す height 属性の値を 64 から 65 に増やす
  • 8 行目:クラスの外で __init__ メソッドを定義できるように、super を修正する。この修正について忘れた方は 以前の記事 を復習すること
 1  def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
 2      self.mbtree = mbtree
 3      self.isscore = isscore
 4      self.size = size
 5      self.width = 50
 6      self.height = 65
 7      self.nodelist = self.mbtree.nodelist_by_score if isscore else self.mbtree.nodelist 
 8      self.nodenum = len(self.nodelist)
 9      super(Mbtree_Anim, self).__init__()
10      
11  Mbtree_Anim.__init__ = __init__
行番号のないプログラム
def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
    self.mbtree = mbtree
    self.isscore = isscore
    self.size = size
    self.width = 50
    self.height = 65
    self.nodelist = self.mbtree.nodelist_by_score if isscore else self.mbtree.nodelist 
    self.nodenum = len(self.nodelist)
    super(Mbtree_Anim, self).__init__()
    
Mbtree_Anim.__init__ = __init__
修正箇所
def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
    self.mbtree = mbtree
    self.isscore = isscore
    self.size = size
    self.width = 50
-   self.height = 64
+   self.height = 65
    self.nodelist = self.mbtree.nodelist_by_score if isscore else self.mbtree.nodelist 
    self.nodenum = len(self.nodelist)
-   super().__init__()
+   super(Mbtree_Anim, self).__init__()
    
Mbtree_Anim.__init__ = __init__

Mbtree_Anim クラスの update_gui メソッドの修正

次に、update_gui メソッドを下記のプログラムのように修正します。

  • 4 行目:Axes の y 軸方向の表示範囲を -1 だけずらして、一番上の局面の評価値が表示されるように修正する。この修正について忘れた方は 以前の記事 を復習すること
  • 7 行目draw_subtre メソッドを呼び出す際に、キーワード引数 size=self.size を記述するようにすることで、評価値の文字が適切な大きさで表示されるように修正する
1  def update_gui(self):
2      self.ax.clear()
3      self.ax.set_xlim(-1, self.width - 1)
4      self.ax.set_ylim(-1, self.height - 1)   
元と同じなので省略
5      self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
6                               anim_frame=self.play.value, isscore=self.isscore, 
7                               ax=self.ax, maxdepth=maxdepth, size=self.size)
元と同じなので省略
8    
9  Mbtree_Anim.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(-1, self.height - 1)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    self.selectednode = self.nodelist[self.play.value]
    self.centernode = self.selectednode
    if self.mbtree.algo == "bf":
        if self.centernode.depth > 0:
            self.centernode = self.centernode.parent
    while self.centernode.depth > 6:
        self.centernode = self.centernode.parent
    if self.centernode.depth <= 4:
        maxdepth = self.centernode.depth + 1
    elif self.centernode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
                             anim_frame=self.play.value, isscore=self.isscore, 
                             ax=self.ax, maxdepth=maxdepth, size=self.size)
                            
    disabled = self.play.value == 0
    self.set_button_status(self.prev_button, disabled=disabled)
    disabled = self.play.value == self.nodenum - 1
    self.set_button_status(self.next_button, disabled=disabled)
    
Mbtree_Anim.update_gui = update_gui
修正箇所
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
-   self.ax.set_ylim(0, self.height)   
+   self.ax.set_ylim(-1, self.height - 1)   
元と同じなので省略
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
                             anim_frame=self.play.value, isscore=self.isscore, 
-                            ax=self.ax, maxdepth=maxdepth, size=self.size)
+                            ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
    
Mbtree_Anim.update_gui = update_gui

上記の修正後に下記のプログラムを実行すると、実行結果のように問題が修正されたことが確認できます。

Mbtree_Anim(mbtree, isscore=True)

実行結果

下記のプログラムのように キーワード引数 size=0.5 を記述して 別の大きさで表示 した場合でも、実行結果のように評価値の文字が適切な大きさで表示されることが確認できます。

Mbtree_Anim(mbtree, isscore=True, size=0.5)

実行結果(大きすぎるので上部のみの画像です)

Mbtree_Anim の改良

現状の Mbtree_Anim では、まだ 評価値を計算していない局面灰色で表示 しますが、上図のように 評価値を計算していない局面評価値が表示 されてしまう点が 不自然 です。そこで、評価値を計算していない局面評価値を表示しない ように改良することにします。

局面の 評価値の表示 は、Node クラスの draw_node メソッドの 仮引数 show_scoreTrue が代入 されている場合に行われます。draw_node メソッドは draw_subtree メソッド内で呼び出されている ので、draw_subtree 内で draw_node を呼び出す際 に、キーワード引数 show_score に適切な値を代入する ようにプログラムを修正すれば良いことがわかります。

draw_node メソッドは、draw_subtree 内の 5 箇所で、下記のプログラムのように記述されて呼び出されているので、それらのプログラムを ノードの評価値が計算されている場合は show_score=Trueそうでない場合は show_score=False を実引数に記述して draw_node を呼び出すように修正すればよいことがわかります。どのように修正すれば良いかについて少し考えてみて下さい。

    darkness = calc_darkness(node)
    rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                          show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)

calc_darkness の復習

上記の darkness は、局面の表示の暗さ を表す値で、calc_darkness によって計算 されます。Mbtree_Anim から draw_subtree を呼び出して ゲーム木の部分木を表示する際 には、評価値が計算されていない局面を暗く 表示し、計算された局面を明るく 表示します。

従って、darkness の値が暗い場合に show_score=False を、そうでない場合は show_score=True を記述して draw_node を呼び出すようにすれば良いことがわかります。

calc_darkness以前の記事で定義した関数ですが、忘れた方も多いと思いますので、その処理を復習することにします。下記は、calc_darkness の定義です。

 1  def calc_darkness(node):
 2      if show_bestmove:
 3          if node.parent is None:
 4              return 0
 5          elif node.mb.last_move in node.parent.bestmoves:
 6              return 0
 7          else:
 8              return 0.2
 9           
10      if anim_frame is None:
11          return 0
12      index = node.score_index if isscore else node.id
13      return 0.5 if index > anim_frame else 0

まず、Mbtree_Anim から draw_subtree が呼び出された際の calc_darkness の処理を復習 します。Mbtree_Anim では、以下のプログラムで draw_subtree を呼び出します

    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
                             anim_frame=self.play.value, isscore=self.isscore, ax=self.ax,
                             maxdepth=maxdepth)

また、draw_subtree の定義 は以下のプログラムのようになっているので、Mbtree_Anim から draw_subtree を呼び出した場合は、仮引数 show_bestmove には False が、anim_frame にはアニメーションの フレーム番号を表す数値 が代入されます。

def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
                 isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8,
                 maxdepth=2):

従って、Mbtree_Anim から draw_subtree を呼び出した場合は、calc_darkness の中の 下記のプログラムだけが実行 されます。評価値の計算過程を表示 する場合は、Mbtree_Anim のインスタンスを作成する際に isscore=True を記述するので、下記の 2 行目で ノードの評価値が計算されていない場合は 0.5 が、計算されている場合は 0 を返す という処理が行われていることがわかります。

1  index = node.score_index if isscore else node.id
2  return 0.5 if index > anim_frame else 0

推奨しないプログラムの修正方法

上記から、ノードの 評価値が計算されていない場合darkness に 0.5 が代入 されるので、draw_subtree 内で draw_node を呼び出す前 に、下記のプログラムの 2、3 行目を追加して、ノードの評価値が計算されていないことを表す darkness に 0.5 が代入されている場合show_scoreFalse を代入 すればよいと思った人がいるかもしれませんが、このような修正は行わないほうが良い でしょう。その理由について少し考えてみて下さい。

1  darkness = calc_darkness(node)
2  if darkness == 0.5:
3      show_score = False
4  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
5                        show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
修正箇所
    darkness = calc_darkness(node)
+   if darkness == 0.5:
+       show_score = False
    rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                          show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)

上記の修正で、確かにプログラムは正しく動作しますが、以下のような問題があります。

第一に、現状の calc_darkness が 0.5 を返り値として返す のは、Mbtree_Anim から draw_subtree が呼び出された際に、ノードの評価値が計算されていない場合だけ ですが、下記のプログラムの 8 行目のように、それ以外の場合で 0.5 を返す ように修正すると プログラムが正しく動作しなくなります。その理由について少し考えてみて下さい。

 1  def calc_darkness(node):
 2      if show_bestmove:
 3          if node.parent is None:
 4              return 0
 5          elif node.mb.last_move in node.parent.bestmoves:
 6              return 0
 7          else:
 8              return 0.5
 9           
10      if anim_frame is None:
11          return 0
12      index = node.score_index if isscore else node.id
13      return 0.5 if index > anim_frame else 0
修正箇所
def calc_darkness(node):
    if show_bestmove:
        if node.parent is None:
            return 0
        elif node.mb.last_move in node.parent.bestmoves:
            return 0
        else:
-           return 0.2
+           return 0.5
           
    if anim_frame is None:
        return 0
    index = node.score_index if isscore else node.id
    return 0.5 if index > anim_frame else 0

例えば、Mbtree_GUI から draw_subtree を呼び出す場合などで、show_bestmove の値が True になる場合 があります。その場合は 最善手でない着手が行われた局面 に対して 8 行目の処理が実行され、返り値として 0.5 が返される ことになります。その結果、最善手でない着手が行われた局面評価値が表示されなくなってしまう という問題が発生します。

このように、0.5 という数値 で評価値が計算されていないノードであるかどうかを 判定する と、別の理由で 0.5 という数値が計算 された場合と 処理が混同 してしまいます。

第二に、評価値が計算されていない局面の 暗さは 0.5 でなくても構わない ので、例えば 暗さを 0.5 から 0.6 に変更しようと思った場合 に行う 修正 が、calc_darkness の最後の行を下記のプログラムのように 変更するだけでは済まない という問題があります。

    return 0.6 if index > anim_frame else 0

具体的には、draw_node を呼び出す直前 の下記の 2 行目のプログラムの 条件式の 0.5 を 0.6 に修正する必要 があります。さらに、draw_subtree 内では draw_node を 5 箇所で呼び出している ので、その全ての場所 で下記の 2 行目のような 修正を行う必要がある点が面倒 です。また、修正し忘れによるバグが発生する可能性が高い という問題もあります。

1  darkness = calc_darkness(node)
2  if darkness == 0.6:
3      show_score = False
4  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
5                        show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
修正箇所
    darkness = calc_darkness(node)
-   if darkness == 0.5:
+   if darkness == 0.6:
        show_score = False
    rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                          show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)

calc_darkness の修正

上記の問題が発生する原因は、局面の表示の暗さ と、評価値の表示の有無同じ darkness という変数の値で 無理に表現しようとした点 にあります。calc_darkness局面の表示の暗さ と、評価値の表示の有無両方を計算して返す ようにすることで、うまく処理を行うことができるようになります。本記事では、0 番の要素 にこれまでに計算していた 局面の暗さを表す数値1 番の要素評価値を表示するか どうかを表す bool 型のデータを持つ tuple を返り値として返す ように calc_darkness を修正することにします。下記はそのように calc_darkness を修正したプログラムです。

  • 4、6、8、10 行目:Mbtree_Anim 以外から draw_subtree が呼び出された場合は、評価値の表示の有無は show_score の値によって決まる ので、その値をそのまま tuple の 1 番の要素 として返すように修正する
  • 13 行目:評価値が計算されていないノードの場合は、tuple の 1 番の要素に False を、そうでない場合は True を返すように修正する
 1  def calc_darkness(node):
 2      if show_bestmove:
 3          if node.parent is None:
 4              return 0, show_score
 5          elif node.mb.last_move in node.parent.bestmoves:
 6              return 0, show_score
 7          else:
 8              return 0.2, show_score
 9           
10      if anim_frame is None:
11          return 0, show_score
12      index = node.score_index if isscore else node.id
13      return (0.5, False) if index > anim_frame else (0, True)
修正箇所
    def calc_darkness(node):
        if show_bestmove:
            if node.parent is None:
-               return 0
+               return 0, show_score
            elif node.mb.last_move in node.parent.bestmoves:
-               return 0
+               return 0, show_score
            else:
-               return 0.2
+               return 0.2, show_score
            
        if anim_frame is None:
-           return 0
+           return 0, show_score
        index = node.score_index if isscore else node.id
-       return 0.5 if index > anim_frame else 0
+       return (0.5, False) if index > anim_frame else (0, True)

行う処理が変わったので、calc_darkness の名前を calc_darkness_and_showmove のように修正したほうが良いかもしれませんが、関数名が長くなるので本記事では名前を変更しないことにします。変更したい人は自由に変更して下さい。

13 行目を下記のプログラムのように、返り値を表す tuple の () を省略 して記述すると エラーが発生する 点に注意して下さい。

    return 0.5, False if index > anim_frame else 0, show_score

その理由は、上記のプログラムが下記のプログラムのように、0.5False if index > anim_frame else 0show_score という 3 つの要素を持つ tuple を返すからです。

    return (0.5, False if index > anim_frame else 0, show_score)

そのことは、下記のプログラムで確認できます。

index = 1
anim_frame = 0 
show_score = True
retval = 0.5, False if index > anim_frame else 0, show_score
print(retval)

実行結果

(0.5, False, True)

show_subtree の修正

次に、show_subtree を下記のプログラムのように修正します。

  • 3、9、12、15 行目cald_darkness の返り値を darknessshow_score に代入するように修正する
  • 7 行目draw_node以前の記事で記述した maxdepth の深さの局面から最善手を着手し続けた場合のノードの表示を行う 処理なので、Mbtree_Anim から draw_subtree を呼び出した場合は 実行されない。そのため、7 行目の直前で calc_darkness を呼び出して darknessshow_score の計算を 行う必要はない。また、このノードは常に明るく表示するため、7 行目の draw_node にはキーワード引数 darkness は記述されていない
 1  def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
 2                   isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 3                  darkness, show_score = calc_darkness(node)
 4                  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
 5                                        show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
 6                          emphasize = bestnode is selectednode
 7                          rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize,
 8                                                    show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
 9                  darkness, show_score = calc_darkness(sibling)
10                  rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness,
11                                           show_score=show_score, lw=lw, dx=dx, dy=dy)
元と同じなので省略
12          darkness, show_score = calc_darkness(parent)
13          rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, 
14                                  show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
15              darkness, show_score = calc_darkness(node)
16              rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness,
17                                    show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
18              self.nodes_by_rect[rect] = node
19            
20  Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
                 isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        if show_bestmove:
            if node.parent is None:
                return 0, show_score
            elif node.mb.last_move in node.parent.bestmoves:
                return 0, show_score
            else:
                return 0.2, show_score
            
        if anim_frame is None:
            return 0, show_score
        index = node.score_index if isscore else node.id
        return (0.5, False) if index > anim_frame else (0, True)
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    if show_bestmove:
        width = 5 * 10
    else:
        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 + 1) * size))
        ax.set_xlim(0, width)
        ax.set_ylim(-1, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    if show_bestmove:
        bestx = 5 * maxdepth + 4
        bestwidth = 50 - bestx
        ax.add_artist(patches.Rectangle(xy=(bestx, -1), width=bestwidth,
                                        height=height + 1, fc="lightgray"))
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness, show_score = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                                    show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                if show_bestmove and depth == maxdepth:
                    bestnode = node
                    while len(bestnode.bestmoves) > 0:
                        bestmove = bestnode.bestmoves[0]
                        bestnode = bestnode.children_by_move[bestmove]
                        dx = 5 * bestnode.depth
                        bestnode.height = 4
                        emphasize = bestnode is selectednode
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize,
                                                show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
                        self.nodes_by_rect[rect] = bestnode                                          
                    
                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
                darkness, show_score = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness,
                                        show_score=show_score, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness, show_score = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, 
                                show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness, show_score = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness,
                                show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
修正箇所
def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
                 isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
-               darkness = calc_darkness(node)
+               darkness, show_score = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                                      show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
                        emphasize = bestnode is selectednode
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize,
                                                  show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
-               darkness  = calc_darkness(sibling)
+               darkness, show_score  = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness,
                                         show_score=show_score, lw=lw, dx=dx, dy=dy)
元と同じなので省略
-       darkness = calc_darkness(parent)
+       darkness, show_score = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, 
                                show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
-           darkness = calc_darkness(node)
+           darkness, show_score = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness,
                                show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree

上記の修正後に下記のプログラムを実行すると、実行結果のように、評価値が計算されていない局面の上に評価値が表示されなくなったことが確認できます。

Mbtree_Anim(mbtree, isscore=True)

実行結果

また、アニメーションのフレームを進めると、下図のように評価値が計算されている局面の上に評価値が表示されることが確認できます。

ゲーム木の作成の経過の表示の確認

上記で Mbtree_Anim の実引数に isscore=True を記述して、評価値の計算過程を表示 するアニメーションが正しく表示されることが確認できました。

プログラムを修正した結果、Mbtree_Anim の実引数に isscore=True を記述しない場合に 表示される ゲーム木の作成の過程の表示バグが発生している可能性がある ので、下記のプログラムを実行して 確認する ことにします。

Mbtree_Anim(mbtree)

実行結果

実行結果から、最初に作成されたルートノードが明るく表示 され、それ以外の まだ作成されていないノードが暗く表示される ことが確認できます。その際に、作成されているノードの上に評価値が表示されていますが、この 評価値は表示しないほうが良い でしょう。その理由について少し考えてみて下さい。

評価値を計算しないほうが良い理由

評価値が計算される順番 は、ゲーム木の ノードが作成される順番と大きく異なります。例えば ルートノードの評価値が計算される のは、他の全てのノードの評価値が計算された後 なので、最初に ルートノードが作成された時点で ルートノードの評価値が表示される のは順番が 間違っています。上記のアニメーションでは ノードが作成された順番で評価値が表示される ため、評価値の計算の順番に対する誤解が生じる可能性が高い でしょう。

そこで、誤解が生じないようにするために、Mbtree_Anim で ゲーム木の生成の過程 のアニメーションを表示する場合は、評価値を表示しない ように修正することにします。どのように修正すれば良いかについて少し考えてみて下さい。

Mbtree クラスの draw_subtree の修正

行いたい修正は、calc_darkness で局面を 明るく表示すると判定 した際に、isscoreTrue の場合は評価値を表示 し、そうでない場合は評価値を表示しない ような 返り値を計算する というものです。

calc_darkness局面を明るく表示すると判定 した場合に 返す値 は、下記のプログラムの else の直後の (0, True) という tuple です。

    return (0.5, False) if index > anim_frame else (0, True)

この部分を isscoreTrue の場合に (0, True)False の場合に (0, False) を返すようにするためには、下記のプログラムのように修正します。

    return (0.5, False) if index > anim_frame else (0, isscore)

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

  • 5 行目:上記のように修正する
1  def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
2                   isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
3      def calc_darkness(node):
元と同じなので省略
4          index = node.score_index if isscore else node.id
5          return (0.5, False) if index > anim_frame else (0, isscore)
元と同じなので省略 
6            
7  Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
                 isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
        if show_bestmove:
            if node.parent is None:
                return 0, show_score
            elif node.mb.last_move in node.parent.bestmoves:
                return 0, show_score
            else:
                return 0.2, show_score
            
        if anim_frame is None:
            return 0, show_score
        index = node.score_index if isscore else node.id
        return (0.5, False) if index > anim_frame else (0, isscore)
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=maxdepth)
    if show_bestmove:
        width = 5 * 10
    else:
        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 + 1) * size))
        ax.set_xlim(0, width)
        ax.set_ylim(-1, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    if show_bestmove:
        bestx = 5 * maxdepth + 4
        bestwidth = 50 - bestx
        ax.add_artist(patches.Rectangle(xy=(bestx, -1), width=bestwidth,
                                        height=height + 1, fc="lightgray"))
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is selectednode
                darkness, show_score = calc_darkness(node)
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
                                    show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                if show_bestmove and depth == maxdepth:
                    bestnode = node
                    while len(bestnode.bestmoves) > 0:
                        bestmove = bestnode.bestmoves[0]
                        bestnode = bestnode.children_by_move[bestmove]
                        dx = 5 * bestnode.depth
                        bestnode.height = 4
                        emphasize = bestnode is selectednode
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize,
                                                show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
                        self.nodes_by_rect[rect] = bestnode                                          
                    
                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
                darkness, show_score  = calc_darkness(sibling)
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness,
                                        show_score=show_score, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        darkness, show_score  = calc_darkness(parent)
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, 
                                show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            darkness, show_score  = calc_darkness(node)
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness,
                                show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
修正箇所
def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
                 isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
    def calc_darkness(node):
元と同じなので省略
        index = node.score_index if isscore else node.id
-       return (0.5, False) if index > anim_frame else (0, True)
+       return (0.5, False) if index > anim_frame else (0, isscore)
元と同じなので省略 
            
Mbtree.draw_subtree = draw_subtree

上記の修正後に下記のプログラムを実行すると、実行結果のように ゲーム木の作成過程を表示 した場合に 明るいノードの上に評価値が表示されなくなった ことが確認できます。

Mbtree_Anim(mbtree)

実行結果

また、実行結果は先ほどと同じなので省略しますが、下記のプログラムを実行して 評価値の計算過程を表示 した場合は、アニメーションのフレームを進めると 明るいノードの上に評価値が表示される ことが確認できます。

Mbtree_Anim(mbtree, isscore=True)

今回の記事のまとめ

今回の記事では、Mbtree_Anim クラスのバグの修正と改良を行いました。

本記事で入力したプログラム

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
tree.py 本記事で更新した tree_new.py

次回の記事

  1. 4 行目で、isscore=True を記述することで、評価値の計算過程を表示します。忘れた方は以前の記事を復習して下さい。

  2. 以前の記事では、局面の上に評価値を表示していなかったので、仮引数 size に適切な値を代入する必要はありませんでした

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?