0
0

Pythonで〇×ゲームのAIを一から作成する その94 ゲーム木を視覚化する GUI の作成

Last updated at Posted at 2024-06-30

目次と前回の記事

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

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

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

ルールベースの AI の一覧

ルールベースの AI の一覧については、下記の記事を参照して下さい。

ゲーム木を視覚化する Mbtree_GUI クラスの作成

前回の記事で、GUI に関する処理を行う基底クラスとなる GUI クラスを作成しましたので、今回の記事ではそれを継承した、ゲーム木の視覚化を行う Mbtree_GUI クラスを定義します。ゲーム木の視覚化する GUI の仕様については、前回の記事を参照して下さい。

draw_tree に関する修正

Mbtree_GUI を定義する前に、draw_tree に関する修正を行うことにします。

draw_tree は最初は startnode で指定したノードからはじまり(start)、指定した深さまでのノードの部分木を描画していたので、仮引数の名前を startnode にしましたが、その後の修正によって、startnode の親ノードも描画する ようになったため、startnode という名前を変えたほうが良いでしょう。現状では、draw_tree は、startnode を中心(center)とした部分木を描画 するので、startnodecenternode という名前に修正することにします。

また、draw_tree が行う処理は、ゲーム木の 部分木(subtree)の描画 ですが、draw_tree という名前では、ゲーム木全体を描画するように勘違いされる可能性が高いでしょう。そこで、メソッドの名前を draw_subtree に修正することにします。

また、以後は draw_tree が行う処理を、「centernode を中心とする部分木を描画する」のように表記することにします。

なお、一般的には仮引数の名前やメソッドの名前を変更すると、そのメソッドを利用する他の関数やメソッドを修正する必要が生じますが、幸いなことに、現時点では他の関数やメソッドから draw_tree を呼び出す処理をどこにも記述していないので、上記の修正を行っても問題は発生しません。

下記は、そのように draw_tree を修正したプログラムです。startnodecenternode に修正する際は、以前の記事で説明した、VSCode の シンボル名の変更の機能を使うと簡単に行える ので、修正した場所を行番号で示す説明は省略します。

修正したプログラム
from tree import Mbtree
import matplotlib.pyplot as plt

def draw_subtree(self, centernode=None, size=0.25, lw=0.8, maxdepth=2):
    if centernode is None:
        centernode = self.root
    self.calc_node_height(maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    fig, ax = plt.subplots(figsize=(width * size, height * size))
    ax.set_xlim(0, width)
    ax.set_ylim(0, height)   
    ax.invert_yaxis()
    ax.axis("off")        
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                node.draw_node(ax=ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=dy)
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
            dy += sibling.height
        dx = 5 * parent.depth
        parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
            
Mbtree.draw_subtree = draw_subtree
修正箇所
from tree import Mbtree
import matplotlib.pyplot as plt

-def draw_tree(self, startnode=None, size=0.25, lw=0.8, maxdepth=2):
+def draw_subtree(self, centernode=None, size=0.25, lw=0.8, maxdepth=2):
-   if startnode is None:
+   if centernode is None:
-       startnode = self.root
+       centernode = self.root
    self.calc_node_height(maxdepth)
    width = 5 * (maxdepth + 1)
-   height = startnode.height
+   height = centernode.height
-   parent = startnode.parent
+   parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    fig, ax = plt.subplots(figsize=(width * size, height * size))
    ax.set_xlim(0, width)
    ax.set_ylim(0, height)   
    ax.invert_yaxis()
    ax.axis("off")        
    
-   nodelist = [startnode]
+   nodelist = [centernode]
-   depth = startnode.depth
+   depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
-           dy = parent.children.index(startnode) * 4
+           dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                node.draw_node(ax=ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=dy)
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
-           if sibling is not startnode:
+           if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
            dy += sibling.height
        dx = 5 * parent.depth
        parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
            
Mbtree.draw_subtree = draw_subtree

上記の修正後に、下記のプログラムでルートノードを中心とする、深さ 2 までの部分木を描画することで、実行結果のように draw_subtree が正しく動作することが確認できます。

mbtree = Mbtree()
mbtree.draw_subtree(mbtree.root, maxdepth=2)
実行結果(縦に長いのでクリックして表示して下さい)

実引数を省略した場合は、ルートノードを中心とする深さ 2 までの部分木を描画するように draw_subtree を定義したので、上記のプログラムは下記のプログラムのように記述できますが、そのことを忘れている人がいるかもしれないので、上記では実引数を記述しました。なお、実行結果は上記と同じなので省略します。

mbtree.draw_subtree()

GUI クラスを継承した Mbtree_GUI クラスの定義

GUI クラスを継承した Mbtree_GUI クラスは、下記のプログラムのように定義します。

  • 3 行目:GUI クラスを基底クラスとする、Mbtree_GUI クラスを定義する
  • 4 行目:Mbtree_GUI クラスでは、〇×ゲームのゲーム木に対する処理を行う必要がある ので、〇×ゲームのゲーム木を表す Mbtree クラスのインスタンスの情報が必要 になる。そこで、その情報を代入する mbtree という仮引数を __init__ メソッドに追加する。また、Mbtree_GUI で表示する部分木の画像を表す Figure のサイズを調整 できるように、size という仮引数を追加することにする
  • 5、6 行目mbtreesize を、同じ名前の属性に代入する
  • 7 行目:基底クラスである GUI クラスの __init__ メソッドの処理を super を使って呼び出すことで、GUI に共通する初期化処理 を行う
  • 9 ~ 22 行目create_widgets などの、GUI クラスの 抽象メソッド と同じ名前のメソッドを定義して オーバーライド する。なお、これらのメソッドの処理はこの後で記述する
 1  from gui import GUI
 2
 3  class Mbtree_GUI(GUI):
 4      def __init__(self, mbtree, size=1):
 5          self.mbtree = mbtree
 6          self.size = size
 7          super().__init__()
 8       
 9      def create_widgets(self):
10          pass
11     
12      def create_event_handler(self):
13          pass
14    
15      def display_widgets(self):
16          pass
17
18      def update_gui(self):
19          pass
20
21      def update_widgets_status(self):
22          pass
行番号のないプログラム
from gui import GUI

class Mbtree_GUI(GUI):
    def __init__(self, mbtree, size=1):
        self.mbtree = mbtree
        self.size = size
        super().__init__()
        
    def create_widgets(self):
        pass
    
    def create_event_handler(self):
        pass
    
    def display_widgets(self):
        pass

    def update_gui(self):
        pass

    def update_widgets_status(self):
        pass

super().__init__()__init__ メソッドのどこで呼び出すかは、状況によって異なります。Mbtree_GUI クラスの場合は、self.mbtreeself.size などの情報を、super().__init__() の中から呼び出されるメソッドで利用する必要がある(例えば、Figure を作成する際に self.size の情報が必要になります)ので、self.size = size などの処理を、super().__init__() の前に記述する必要があります。

逆に、super().__init__() の中で行った処理を利用するプログラムを __init__ メソッド内に記述する必要がある場合は、それらの処理は super().__init__() の後に記述する必要があります。

create_widgets メソッドの定義

次に、GUI クラスの抽象メソッドをオーバーライドする、create_widgets などのメソッドが行う処理を具体的に記述する必要があります。

ウィジェットがなければ Mbtree_GUI の処理は行えないので、最初にGUI に表示する ウィジェットを作成する create_widgets メソッドを定義 します。

前回の記事で、Mbtree_GUI では、下記の 4 つのボタンを利用することに決めたので、その 4 つのボタンのウィジェットを作成する必要 があります。また、ゲーム木は matplotlib の Figure に描画するので、Figure を作成する必要 もあります。

ボタンの表示と対応するカーソルキー
親ノードへ移動
一つ前の兄弟ノードへ移動
一つ後の兄弟ノードへ移動
子孫ノードの中の先頭のノードへ移動

ボタンのウィジェットの作成

まず、4 つのボタンのウィジェットを作成することにします。そのためには、それぞれのボタンのウィジェットを代入する Mbtree_GUI クラスの 属性の名前を決める必要 があり、本記事ではボタンに表示する矢印の方向から、下記のように命名しました。

ボタンの表示 属性の名前
left_button
up_button
right_button
down_button

ボタンのウィジェットは、基底クラスである GUI クラスcreate_button メソッドを使って作成できるので、create_widgets は下記のプログラムのように記述できます。なお、本記事ではボタンの幅を 100 ピクセルに設定しましたが、自由に変更してもかまいません。

def create_widgets(self):
    self.left_button = self.create_button("", 100)
    self.up_button = self.create_button("", 100)
    self.right_button = self.create_button("", 100)
    self.botton_button = self.create_button("", 100)
    
Mbtree_GUI.create_widgets = create_widgets

Figure の大きさの計算

部分木を描画する Figure を作成するため には、Figure の大きさを決める必要 があります。どのようにしてその大きさを決めることができるかについて少し考えてみて下さい。

Mbtree_GUI では、draw_subtree を使って、下図のような部分木を描画します。赤丸が、draw_subtree で描画を行う際に仮引数 centernode に代入したノードです。

draw_subtree が描画する 部分木 は、draw_subtree の仮引数 centernodemaxdepth に代入される値によって大きく変化 します。例えば、上図の部分木と、先程描画したルートノードを中心とする深さ 2 までの部分木では、縦幅も横幅も大きく異なります

部分木の描画 は、Figure に登録された Axes に対して行います。そのため、Axes の表示範囲centernodemaxdepth にどのような値が代入された場合 でも、draw_subtree が描画する 部分木がすべて収まるだけの大きさに設定する 必要があります。そのためには、部分木の縦幅と横幅の最大値 を調べる必要があります。

Axes の表示範囲が決まれば、Figure の大きさ を Axes の表示範囲の 縦幅と横幅と同じ比率で設定すればよい ことがわかる(そうしなければ、縦と横の縮尺が異なる、ゆがんだ部分木が描画されてしまいます)ので、Axes の表示範囲を考えることにします。

draw_subtree が描画する部分木の大きさに合わせて、Figure のサイズを変更するという方法も考えられますが、Figure のサイズを変化させると、JupyterLab のセルの大きさが変化するため、見た目があまりよくないと思いましたので、本記事では Figure のサイズを Figure の作成後に変更する方法は採用しません。

まず、部分木の 横幅の最大値 を考えることにします。draw_subtree では、深さ 0 のノードから、最大で深さ 9 までのノードを描画するので、最大で横に深さが 0 から 9 までの 10 個のゲーム盤が描画される ことになります。draw_subtree では、ゲーム盤を 横方向に間を 2 だけ開けて描画する ので、最大で 5 * 10 = 50 の幅が必要 になることが分かります。

次に、部分木の 縦幅の最大値 を考えることにします。上図では、赤丸の centernode から深さ 9 までの部分木を描画していますが、centernode の深さが浅い場合に、深いノードまでの部分木を描画 した場合は、縦方向に描画しなければならない ノードの数が多くなりすぎる ことになります。例えば、以前の記事で説明したように、深さ 0 から深さ 9 までの ゲーム木全体を描画 すると、ゲーム木の高さが 100 万を超える ことになります。

部分木の高さ は、draw_subtree で描画する 部分木の深さを浅くすることで減らす ことができます。centernode の子ノードまでの部分木が、最も浅い部分木になるので、Mbtree_GUI では下図のように、centernode の子ノードまでの部分木を描画 することにします。

次に、centernode の子ノードまでを描画した場合の部分木の高さの最大値を考えることにします。どのように計算するかがわからない場合は、具体例を使って部分木の高さを計算する と良いでしょう。そこで、最初に上図の部分木の高さの計算方法について考えることにします。どのように計算すればよいかについて少し考えてみて下さい。

まず、centernode とその子ノードを表す、下図の赤枠の中に、ゲーム盤が最大でいくつ縦に並ぶかを計算します。

その数は、centernode の子ノードの数によって変化 します。centernode の深さ(depth)を d とすると、深さ d のノードは d 手目の局面なので、合法手の数9 - d になります。ただし、決着がついた局面には合法手が存在しないので、その数は下記のようになります。

  • centernode の局面が、決着がついた局面 の場合は 子ノードは存在しない ので、赤枠の中には centernode が 1 つだけ描画 される
  • 決着がついていない 場合は、赤枠の中に 子ノードが 9 - d 個縦に並んで描画 される

〇×ゲームでは、深さが 9 の局面 は必ず決着がついた局面になります。そのため、上記から、赤枠の中に並ぶゲーム盤の最大値 は以下のようになります。

  • 深さ d が 9 の場合は 1
  • それ以外の場合は 9 - d

次に下図の緑枠の中に、ゲーム盤が最大でいくつ縦に並ぶかを計算します。

図から、緑枠の高さ は、赤枠の高さ に、centernode兄弟ノードの高さを加えたもの であることがわかります。ただし、深さが 0 のルートノードだけは 親ノードを持たない ので、兄弟ノードは存在せず、緑枠の高さは赤枠の高さと同じになる 点に注意して下さい。

d が 0 以外の場合に、深さ d のノードの 親ノードの深さは d - 1 なので、centernode の親ノードは 9 - (d - 1) = 10 - d 個 の子ノードを持つことになります。centernodeその中の 1 つ なので、centernode兄弟ノードの数 は 10 - d - 1 = 9 - d で計算できます。

従って、上図の緑枠の中に縦に並ぶゲーム盤の数の最大値は、赤枠の中の最大値 + 兄弟ノードの数 になるので、以下のようになります。

d 赤枠の最大値 兄弟ノードの数 最大値
0 9 - 0 = 9 0 9
1 ~ 8 9 - d 9 - d 18 - 2 * d
9 1 9 - 9 = 0 1

0 から 9 までのそれぞれの最大値を計算すると以下のようになるので、部分木では、最大で縦に 16 個 のゲーム盤が並べて描画されることが分かります。

d 0 1 2 3 4 5 6 7 8 9
最大値 9 16 14 12 10 8 6 4 2 1

draw_subtree では、ゲーム盤を横方向に間を 1 だけ開けて描画するので、最大で 4 * 16 = 64 の高さが必要 になることが分かります。

従って、Axes の表示範囲 の x、y 座標をそれぞれ 0 ~ 50、と 0 ~ 64 に設定し、Figure の大きさを 横 50、縦 64 の比率で設定すればよい ことがわかりました。

この 50 と 64 という数値は、Figure の作成と Axes の表示範囲を設定する際に必要となるので、下記のプログラムの 5、6 行目のように、__init__ メソッド内で、widthheight という 属性に代入する ことにします。

なお、前回の記事で説明したように、super はクラスの定義の中で記述する必要がある ので、これまでのように、通常の関数として __init__ を定義し、Mbtree_GUI.__init__ = __init__ を実行して __init__定義する事はできない点に注意 して下さい。

Mbtree_GUI というクラスを 定義し直した ので、先程修正した create_widgets の定義 は、下記のプログラムの 9、10 行目の create_widgets の定義で上書き されて 無効になる 点にも注意して下さい。ただし、create_widgets は、この すぐ後で定義し直す ので、下記では何も行わない関数として create_widgets を定義しました。

 1  class Mbtree_GUI(GUI):
 2      def __init__(self, mbtree, size=1):
 3          self.mbtree = mbtree
 4          self.size = size
 5          self.width = 50
 6          self.height = 64
 7          super().__init__()
 8       
 9      def create_widgets(self):
10          pass
元と同じなので省略
行番号のないプログラム
class Mbtree_GUI(GUI):
    def __init__(self, mbtree, size=1):
        self.mbtree = mbtree
        self.size = size
        self.width = 50
        self.height = 64
        super().__init__()
        
    def create_widgets(self):
        pass
    
    def create_event_handler(self):
        pass
    
    def display_widgets(self):
        pass

    def update_gui(self):
        pass

    def update_widgets_status(self):
        pass
修正箇所
class Mbtree_GUI(GUI):
    def __init__(self, mbtree, size=1):
        self.mbtree = mbtree
        self.size = size
+       self.width = 50
+       self.height = 64
        super().__init__()
元と同じなので省略

下記は、Figure を作成する処理を create_widgets に加えた プログラムです。なお、Marubatsu_GUI では、create_widgets の記述が長くなってしまうため、create_figure というメソッドを別途定義して Figure を作成しましたが、Mbtree_GUI クラスの create_widgets は記述が短いので create_widgets の中にその処理を記述しました。

  • 7 行目以前の記事で説明したように、%matplotlib widget を実行した場合に display で Figure を描画する場合は、Figure が 2 回表示されないようにするために、matplotlib のインタラクティブモードを OFF にした状態で Figure を作成する必要があり、with plt.ioff(): のブロック内でで Figure を作成することでそれを行っている
  • 8、9 行目self.widthself.height に、Figure の大きさを調整するための self.size を乗算した大きさの Figure を作成する
  • 10 ~ 13 行目以前の記事で説明した方法で、Figure に対して、タイトルやツールバーなどの表示が行われないようにする
 1  def create_widgets(self):
 2      self.left_button = self.create_button("", 100)
 3      self.up_button = self.create_button("", 100)
 4      self.right_button = self.create_button("", 100)
 5      self.down_button = self.create_button("", 100)
 6   
 7      with plt.ioff():
 8          self.fig, self.ax = plt.subplots(figsize=[self.width * self.size,
 9                                                    self.height * self.size])
10      self.fig.canvas.toolbar_visible = False
11      self.fig.canvas.header_visible = False
12      self.fig.canvas.footer_visible = False
13      self.fig.canvas.resizable = False       
14    
15  Mbtree_GUI.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
    self.left_button = self.create_button("", 100)
    self.up_button = self.create_button("", 100)
    self.right_button = self.create_button("", 100)
    self.down_button = self.create_button("", 100)
    
    with plt.ioff():
        self.fig, self.ax = plt.subplots(figsize=[self.width * self.size,
                                                  self.height * self.size])
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False       
    
Mbtree_GUI.create_widgets = create_widgets
修正箇所
def create_widgets(self):
    self.left_button = self.create_button("", 100)
    self.up_button = self.create_button("", 100)
    self.right_button = self.create_button("", 100)
    self.down_button = self.create_button("", 100)
    
+   with plt.ioff():
+       self.fig, self.ax = plt.subplots(figsize=[self.width * self.size,
+                                                 self.height * self.size])
+   self.fig.canvas.toolbar_visible = False
+   self.fig.canvas.header_visible = False
+   self.fig.canvas.footer_visible = False
+   self.fig.canvas.resizable = False       
    
Mbtree_GUI.create_widgets = create_widgets

display_widgets メソッドの定義

次に、create_widgets で作成した ウィジェットを配置して表示 する display_widgets メソッドを定義します。そのためには、4 つのボタンをどのように配置するかを決める必要があります。4 つのボタンをゲーム機のコントローラーの十字キーのように、上下左右の位置に配置することは可能ですが、一般的に、ボタンなどの GUI のレイアウト は、GUI を実装する際に 最初に厳密に決めなければならないものではありません。その理由は、一生懸命厳密なレイアウトを決めたとしても、後から GUI にボタンなどの機能を追加したりした場合に、せっかく厳密に決めたレイアウトを一から設計し直す必要が生じるからです。そのため、一般的には、厳密な GUI のレイアウトは、GUI の機能をすべて実装してから行うと良い でしょう。本記事では、とりあえず ←、→、↑、↓ の順で横に並べて配置することにします。また、ゲーム木を描画する Figure はその下に配置することにします。

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

  • 4 行目:4 つのボタンを横に並べて配置した Hbox を作成する
  • 5 行目:上記の Hbox と Figure を縦に並べた VBox を作成し、display で表示する
1  import ipywidgets as widgets
2
3  def display_widgets(self):
4      hbox = widgets.HBox([self.left_button, self.right_button, self.up_button, self.down_button])
5      display(widgets.VBox([hbox, self.fig.canvas]))    
6    
7  Mbtree_GUI.display_widgets = display_widgets
行番号のないプログラム
import ipywidgets as widgets

def display_widgets(self):
    hbox = widgets.HBox([self.left_button, self.right_button, self.up_button, self.down_button])
    display(widgets.VBox([hbox, self.fig.canvas]))    
    
Mbtree_GUI.display_widgets = display_widgets

上記の修正後に、下記のプログラムを実行して、Mbtree_GUI クラスのインスタンスを作成すると、実行結果のように、上部に 4 つのボタンが表示され、その下に 横 50 縦 64 の非常に大きな Figure されるようになります。Figure が大きすぎるので、下記の実行結果には 4 つのボタンの部分だけを表記します。なお、作成したインスタンスは今回の記事では特に利用しませんが、mbtree_gui という変数に代入することにします。

mbtree_gui = Mbtree_GUI(mbtree)

実行結果

Figure の大きさを調整するために、キーワード引数 size に様々な値を記述して上記のプログラムを実行した所、下記のように、実引数に size=0.15 を指定して Mbtree_GUI のインスタンスを作成すると、ちょうど良い大きさの Figure が表示されるようになることがわかりました。そこで、後で __init__ メソッドの仮引数 size をデフォルト値が 0.15 のデフォルト引数に修正することにします。他の値が良いと思った人は自由に変更して下さい。

mbtree_gui = Mbtree_GUI(mbtree, size=0.15)

実行結果

update_gui メソッドの定義

次に、GUI の表示 を行う update_gui を定義して、Figure に draw_subtree を使って部分木を描画する ようにします。そのためには、部分木の描画を行う draw_subtree を修正する必要があります。何を修正する必要があるかについて少し考えてみて下さい。

draw_subtree の修正

Mbtree クラスの draw_subtree メソッドは、下記のプログラムのように、その中で部分木を描画する Figure と Axes を作成 していますが、Mbtree_GUI では、部分木を create_widgets で作成した Figure の Axes に対して描画を行う 必要があります。

def draw_subtree(self, centernode=None, size=0.25, lw=0.8, maxdepth=2):

    fig, ax = plt.subplots(figsize=(width * size, height * size))

そこで、draw_subtree に、デフォルト値を None とする仮引数 ax を追加し、axNone の場合のみ draw_subtree 内で Figure を作成する ように修正することにします。なお、この修正は、以前の記事draw_node に対して行ったものと同じものです。

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

  • 1 行目:デフォルト値を None とするデフォルト引数 ax を追加する
  • 5 ~ 10 行目axNone の場合に、Figure の作成などの処理を行うようにする
 1  def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 2      if parent is not None:
 3          height += (len(parent.children) - 1) * 4
 4          parent.height = height
 5      if ax is None:
 6          fig, ax = plt.subplots(figsize=(width * size, height * size))
 7          ax.set_xlim(0, width)
 8          ax.set_ylim(0, height)   
 9          ax.invert_yaxis()
10          ax.axis("off")        
元と同じなので省略
11            
12  Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
    if centernode is None:
        centernode = self.root
    self.calc_node_height(maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                node.draw_node(ax=ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=dy)
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
            dy += sibling.height
        dx = 5 * parent.depth
        parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
            
Mbtree.draw_subtree = draw_subtree
修正箇所
-def draw_subtree(self, centernode=None, size=0.25, lw=0.8, maxdepth=2):
+def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
-   fig, ax = plt.subplots(figsize=(width * size, height * size))
-   ax.set_xlim(0, width)
-   ax.set_ylim(0, height)   
-   ax.invert_yaxis()
-   ax.axis("off")        
+   if ax is None:
+       fig, ax = plt.subplots(figsize=(width * size, height * size))
+       ax.set_xlim(0, width)
+       ax.set_ylim(0, height)   
+       ax.invert_yaxis()
+       ax.axis("off")        
元と同じなので省略
            
Mbtree.draw_subtree = draw_subtree

update_gui の定義

最終的には Figure に描画する部分木を、ボタンで移動できるようにする必要がありますが、ボタンの イベントハンドラをまだ記述していない ので、現時点 では 常にルートノードを中心とした深さ 1 までの部分木を描画 することにします。下記は、そのように update_gui を記述したプログラムです。

  • 2 行目:それまでに Axes に描画されていた内容をクリアする。ax.clear() で、Axes の描画範囲や、上下の反転などの設定がリセットされるので、この後で設定し直す必要がある
  • 3、4 行目:Axes の描画範囲を、先程計算した範囲に設定する
  • 5、6 行目:Axes の y 軸の上下を反転させ、目盛りを表示しないようにする
  • 7 行目draw_subtree で、ルートノードを中心とする、深さ 1 までの部分木を描画する
1  def update_gui(self):
2      self.ax.clear()
3      self.ax.set_xlim(0, self.width)
4      self.ax.set_ylim(0, self.height)   
5      self.ax.invert_yaxis()
6      self.ax.axis("off")   
7      self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
8    
9  Mbtree_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(0, self.width)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
    
Mbtree_GUI.update_gui = update_gui

上記の修正後に、下記のプログラムを実行すると、実行結果のように部分木は描画されません。その理由を少し考えてみて下さい。

mbtree_gui = Mbtree_GUI(mbtree, size=0.15)

実行結果

部分木が描画されない理由と修正

部分木が描画されない理由は、Mbtree_GUI クラスの インスタンスを作成した際に実行 される __init__ メソッドの中で、draw_gui を呼び出す処理を記述していない からです。

以前の記事で Marubatsu_GUI のインスタンスを作成した場合はゲーム盤が表示されるのに、Mbtree_GUI では描画されないのはおかしいと思った方がいるかもしれません。Marubatsu_GUI の場合は、以下のような理由でゲーム盤が描画されます。

  • Marubatsu_GUI クラスのインスタンスは、Marubatsu クラスの play メソッドの中の下記の 4 行目部分で作成される。
1  def play(self, ):

2      # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
3      if gui:
4          mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, seed=seed, size=size)  
5      else:
6          mb_gui = None
7            
8      self.restart()
9      return self.play_loop(mb_gui)
  • その後で、上記の 9 行目で play_loop メソッドが呼び出される
  • play_loop の中の下記のプログラムの 6 行目で、update_gui が呼び出される
1  def play_loop(self, mb_gui):

2          if verbose:
3              if gui:
4                  # AI どうしの対戦の場合は画面を描画しない
5                  if ai[0] is None or ai[1] is None:
6                      mb_gui.update_gui()

つまり、Marubatsu_GUI の場合は、Marubatsu_GUI のインスタンスを作成した際にゲーム盤が描画されているわけではなく、作成後に Marubatsu クラスの play_loop 内で update_gui が呼び出されてゲーム盤が描画されているということです。

そこで、下記のプログラムの 9 行目のように、GUI クラスの __init__ メソッドを、draw_widgets を実行してウィジェットを配置した後で update_gui を呼び出すようにすることで、GUI の表示を更新する処理を行うように修正することにします。

 1  def __init__(self):
 2      # %matplotlib widget のマジックコマンドを実行する
 3      get_ipython().run_line_magic('matplotlib', 'widget')
 4   
 5      self.disable_shortcutkeys()
 6      self.create_widgets()
 7      self.create_event_handler()
 8      self.display_widgets() 
 9      self.update_gui()
10    
11  GUI.__init__ = __init__
行番号のないプログラム
def __init__(self):
    # %matplotlib widget のマジックコマンドを実行する
    get_ipython().run_line_magic('matplotlib', 'widget')
    
    self.disable_shortcutkeys()
    self.create_widgets()
    self.create_event_handler()
    self.display_widgets() 
    self.update_gui()
    
GUI.__init__ = __init__
修正箇所
def __init__(self):
    # %matplotlib widget のマジックコマンドを実行する
    get_ipython().run_line_magic('matplotlib', 'widget')
    
    self.disable_shortcutkeys()
    self.create_widgets()
    self.create_event_handler()
    self.display_widgets() 
+   self.update_gui()
    
GUI.__init__ = __init__

上記の修正後に、下記のプログラムを実行すると、実行結果のようにルートノードを中心とした部分木が描画されるようになります。

mbtree_gui = Mbtree_GUI(mbtree, size=0.15)

実行結果

Axes の配置位置の調整

ところで、上記の実行結果で 部分木の描画位置 が、4 つのボタンより かなり下に表示 されている点が気になる人はいないでしょうか。これは、Figure の中に作成された Axes の配置の位置が原因 ですが、上図では、Figure、Axes の背景色が白色なので、Figure と Axes の範囲が見た目からはわかりません。

Axes の配置位置の確認

そこで、Figure の中で、Axes がどのような位置に配置されているかを確認する ために、下記のプログラムのように Figure の背景色を灰色にし、Axes の軸の目盛りを表示するように update_gui を修正してみることにします。なお、Figure の背景色の変更は、2 行目のように、set_facecolor というメソッドで行うことができます。

  • 2 行目:Figure の背景色を薄い灰色(lightgray)にする
  • 7 行目:軸の目盛りを表示しないようにする処理をコメントにして実行しないようにする。この部分は一時的に実行しないようにするだけなので、コメントにした
 1  def update_gui(self):
 2      self.fig.set_facecolor("lightgray")
 3      self.ax.clear()
 4      self.ax.set_xlim(0, self.width)
 5      self.ax.set_ylim(0, self.height)   
 6      self.ax.invert_yaxis()
 7  #   self.ax.axis("off")   
 8
 9      self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
10    
11  Mbtree_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
    self.fig.set_facecolor("lightgray")
    self.ax.clear()
    self.ax.set_xlim(0, self.width)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
#   self.ax.axis("off")   
    self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
    
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
+   self.fig.set_facecolor("lightgray")
    self.ax.clear()
    self.ax.set_xlim(0, self.width)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
-   self.ax.axis("off")   
+#  self.ax.axis("off")   
    self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
    
Mbtree_GUI.update_gui = update_gui

set_facecolor の詳細については、下記のリンク先を参照して下さい。

上記の修正後に、下記のプログラムを実行すると、実行結果のような画像が描画されます。

mbtree_gui = Mbtree_GUI(mbtree, size=0.15)

実行結果

上図の 灰色の部分が Figure を、内部の 枠の中の白い部分が Axes を表しており、Figure は 4 つのボタンのすぐ下に配置されていることが分かります。従って、部分木が 4 つのボタンよりかなり下に描画されるのは、Axes が Figure のかなり内側に配置されている ことが原因であることがわかります。

%matplotlib widget による Axes の配置位置の変化

筆者が試行錯誤して調べてみた所、%matplotlib widget を実行するか どうかで subplots メソッドで作成した Axes が Figure の内部のどこに配置されるかが若干異なる ことがわかりました。%matplotlib inline を実行すると、%matplotlib widget を実行する前の状態に戻すことができるので、そのことは下記の 2 つのプログラムで確認することができます。

下記は、%matplotlib widget を実行しない状態で、subplots メソッドで Figure と Axes を作成して表示するプログラムです。

%matplotlib inline
plt.close("all")
fig, ax = plt.subplots(facecolor="lightgray")

実行結果

上記の 2 行目の意味は説明しないとわからないと思いますので説明します。下記に関する内容は以前の記事でも説明したので、忘れた方はそちらも見て下さい。

%matplotlib widget を実行しない 場合は、JupyterLab のセルを実行すると、そのセルで作成された Figure は 自動的に描画された後で閉じられて利用できなくなります

一方、%matplotlib widget を実行した後 では、JupyterLab のセルを実行しても、作成した Figure は 自動的に閉じられず、残り続ける ことになります。これは、Figure を閉じてしまうと、後から Figure の内容を変更できなくなるからです。

今回の記事では、Mbtree_GUI クラスを作成する際に、%matplotlib widget が実行され、多くの Figure を描画しました。そのため、それらの Figure が残り続けています。その後で、上記のプログラムのように、%matplotlib inline を実行して JupyterLab のセルを実行すると、それまでに作成されたFigure が全て描画されてしまいます。2 行目の plt.close("all") は、それまでに作成した Figure を全て閉じるという処理を行います。そうすることで、セルの実行後にこれまでに作成した 余計な Figure が描画されなくなります

plt.close の詳細については、下記のリンク先を参照して下さい。

下記は、%matplotlib widget を実行した状態で、subplots メソッドで Figure と Axes を作成した場合のプログラムです。

%matplotlib widget
fig, ax = plt.subplots(facecolor="lightgray")

実行結果

下記は、%matplotlib widget を実行しない場合と、実行した場合で表示される Figure を並べたものです。比べてみるとわかるように、%matplotlib widget を実行した場合 のほうが、Axes が Figure のより内側に作成される ようです。このようなことが起きる原因については筆者の調べた限りではよくわかりませんでした1が、この問題は、Axesplt.subplots ではない、別の方法で作成する ことで解決することができます。

 

add_axes メソッドによる Axes の配置位置の設定

plt.subplots は 1 つの Axes が配置された Figure を作成する場合だけでなく、(本記事ではまだそのような処理を行ったことはありませんが)複数の Axes2 が規則正しく並べて配置される ような Figure を作成する際で便利なので良く使われますが、Figure の中での Axes の配置 をどのような大きさで、どのような位置に行うかを 厳密に指定して作成することはできません。そのような場合は、Figure の add_axes というメソッドを利用する必要があります。add_axes実引数 には、Figure の中に配置する Axes の 位置と大きさ(left, bottom, width, height) という 4 つの要素を持つ tuple で記述 します。

ぞれぞれの要素の意味は以下の通りです。

意味
left Figure の左端を 0、右端を 1 とする座標
bottom Figure の下端を 0、上端を 1 とする座標
width Figure の幅を 1 とした場合の Axes の幅
height Figure の高さを 1 とした場合の Axes の高さ

plt.axes((left, bottom, width, height)) で、current figure に対して同様の方法で Axes を追加することができます。

add_axes は、Figure のメソッド なので、利用する場合は、下記のプログラムのように、plt.figure() で Figure だけを作成した後 で、その Figure に対して add_axes を呼び出します。下記は、Figure の下半分と、右上に Axes を作成するプログラムです。

fig = plt.figure()
ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.4]) # 左下が (0.1, 0.1)、幅が 0.8、高さが 0.4 の Axes
ax2 = fig.add_axes([0.6, 0.6, 0.3, 0.3]) # 左下が (0.6, 0.3)、幅が 0.3、高さが 0.3 の Axes

実行結果

上記のプログラムでは行っていませんが、ax1ax2 を使って、2 つの Axes にそれぞれ別の画像を描画することができます。

Figure の add_axes メソッドの詳細については、下記のリンク先を参照して下さい。

plt.axes() の詳細については、下記のリンク先を参照して下さい。

create_widgets の修正

下記は、Axes を Figure 全体に配置するように create_widgets を修正するプログラムです。

  • 5 行目:Figure の左下の (0, 0) を基準に、幅 1、高さ 1 の範囲を表すように add_axes を記述することで、Figure 全体に配置するように Axes を作成する
1  def create_widgets(self):
元と同じなので省略
2      with plt.ioff():
3          self.fig = plt.figure(figsize=[self.width * self.size,
4                                         self.height * self.size])
5          self.ax = self.fig.add_axes([0, 0, 1, 1])
元と同じなので省略
6    
7  Mbtree_GUI.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
    self.left_button = self.create_button("", 100)
    self.up_button = self.create_button("", 100)
    self.right_button = self.create_button("", 100)
    self.down_button = self.create_button("", 100)
    
    with plt.ioff():
        self.fig = plt.figure(figsize=[self.width * self.size,
                                       self.height * self.size])
        self.ax = self.fig.add_axes([0, 0, 1, 1])   
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False       
    
Mbtree_GUI.create_widgets = create_widgets
修正箇所
def create_widgets(self):
元と同じなので省略
    with plt.ioff():
        self.fig = plt.figure(figsize=[self.width * self.size,
                                       self.height * self.size])
+       self.ax = self.fig.add_axes([0, 0, 1, 1])
元と同じなので省略
    
Mbtree_GUI.create_widgets = create_widgets

また、下記のプログラムのように、update_gui を元に戻します。

def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(0, self.width)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   

    self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
    
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
-   self.fig.set_facecolor("lightgray")
    self.ax.clear()
    self.ax.set_xlim(0, self.width)
    self.ax.set_ylim(0, self.height)   
    self.ax.invert_yaxis()
-#  self.ax.axis("off")   
+   self.ax.axis("off")   

    self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
    
Mbtree_GUI.update_gui = update_gui

上記の修正後に、下記のプログラムを実行すると、実行結果のように部分木が 4 つのボタンのすぐ下に描画されるようになります。

mbtree_gui = Mbtree_GUI(mbtree, size=0.15)

実行結果

実は、Marubatsu_GUI クラスが作成する ゲーム盤を表示する Figure も、Axes が Figure のかなり内側に配置 されています。ただし、Marubatsu_GUI の場合は、ゲーム盤の上と下にメッセージを表示する必要がある ので、Axes が Figure のかなり内側に描画されても問題はありません。ただし、Marubatsu_GUI を定義した際に Axes が Figure のかなり内側に描画されることを筆者は知らなかったので、それは筆者が意図したものではなく、偶然そのようになっていたに過ぎません。

選択中のノードを表す属性の追加と update_gui の修正

Mbtree_GUI では、ボタンによって描画する部分木の 中心となるノードを変更 しますが、そのためには、どのノードが中心となるノードであるか を表す情報を 記録する必要 があります。そこで、本記事では centernode という属性にその情報を代入する ことにします。

次に、centernode 属性の初期化の処理__init__ メソッド内で記述する必要があります。初期設定 では centernodeルートノードを設定するのが自然 だと思いますので、本記事では下記のプログラムのように __init__ メソッドを修正することにします。なお、ついでに 仮引数 sizeデフォルト値を 0.15 とするデフォルト引数に変更 しました。

また、update_gui を、centernode 属性を中心 とし、centernode次の深さまで を描画するように修正しました。なお、ゲーム木の深さは 9 までしかない ので、maxdepth が 9 を超えないように しないとエラーが発生してしまう可能性が生じる点に注意して下さい。

なお、super が記述された __init__ メソッドを変更するので、下記のプログラムのように、Mbtree_GUI クラスを定義しなおしました。

  • 7 行目centernode 属性にルートノードを代入して初期化する
  • 18 行目centernode の深さ + 1 を計算して maxdepth に代入する。ただし、max を使って深さが 9 を超えないようにする
  • 19 行目centernode を中心とする、深さ maxdepth までの部分木を描画する
 1  class Mbtree_GUI(GUI):
 2      def __init__(self, mbtree, size=0.15):
 3          self.mbtree = mbtree
 4          self.size = size
 5          self.width = 50
 6          self.height = 64
 7          self.centernode = self.mbtree.root
 8          super().__init__()
 9
元と同じなので省略
10
11      def update_gui(self):
12          self.ax.clear()
13          self.ax.set_xlim(0, self.width)
14          self.ax.set_ylim(0, self.height)   
15          self.ax.invert_yaxis()
16          self.ax.axis("off")   
17        
18          maxdepth = min(self.centernode.depth + 1, 9)
19          self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)
20        
元と同じなので省略
行番号のないプログラム
class Mbtree_GUI(GUI):
    def __init__(self, mbtree, size=0.15):
        self.mbtree = mbtree
        self.size = size
        self.width = 50
        self.height = 64
        self.centernode = self.mbtree.root
        super().__init__()
        
    def create_widgets(self):
        self.left_button = self.create_button("", 100)
        self.up_button = self.create_button("", 100)
        self.right_button = self.create_button("", 100)
        self.down_button = self.create_button("", 100)
        
        with plt.ioff():
            self.fig = plt.figure(figsize=[self.width * self.size,
                                           self.height * self.size])
            self.ax = self.fig.add_axes([0, 0, 1, 1])
        self.fig.canvas.toolbar_visible = False
        self.fig.canvas.header_visible = False
        self.fig.canvas.footer_visible = False
        self.fig.canvas.resizable = False       
        
    def create_event_handler(self):
        pass
    
    def display_widgets(self):
        hbox = widgets.HBox([self.left_button, self.right_button, self.up_button, self.down_button])
        display(widgets.VBox([hbox, self.fig.canvas]))  

    def update_gui(self):
        self.ax.clear()
        self.ax.set_xlim(0, self.width)
        self.ax.set_ylim(0, self.height)   
        self.ax.invert_yaxis()
        self.ax.axis("off")   
        
        maxdepth = min(self.centernode.depth + 1, 9)
        self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)

    def update_widgets_status(self):
        pass
修正箇所
class Mbtree_GUI(GUI):
    def __init__(self, mbtree, size=0.15):
        self.mbtree = mbtree
        self.size = size
        self.width = 50
        self.height = 64
+       self.centernode = self.mbtree.root
        super().__init__()

元と同じなので省略

    def update_gui(self):
        self.ax.clear()
        self.ax.set_xlim(0, self.width)
        self.ax.set_ylim(0, self.height)   
        self.ax.invert_yaxis()
        self.ax.axis("off")   
        
+       maxdepth = min(self.centernode.depth + 1, 9)
-       self.mbtree.draw_subtree(self.mbtree.root, ax=self.ax, maxdepth=1)
+       self.mbtree.draw_subtree(self.centernode, ax=self.ax, maxdepth=maxdepth)
        
元と同じなので省略

実行結果は先ほどと同じなので省略しますが、上記の修正後に、下記のプログラムを実行すると、ルートノードを中心とした、深さ 1 までの部分木が表示されます。

mbtree_gui = Mbtree_GUI(mbtree)

create_event_handler の定義

create_event_handler には、以下の内容を記述します。下の 2 つは、Marubatsu_GUI の create_event_handler と同様の方法で記述できます。

  • 4 つのボタンをクリックした際に呼び出されるイベントハンドラの定義
  • 4 つのボタンとイベントハンドラの結び付け
  • 4 つのボタンに対応するキーを押した時に対応するイベントハンドラを呼び出す処理

4 つのボタンに対する処理を、順番に記述する事にします。

親ノードへの移動

← ボタンを押した時に行う、部分木の中心となる centernode 属性のノードを 親のノードへ移動 する処理は、以下の手順で行うことができます。最後の Figure の描画の更新の処理の記述を忘れないように注意 して下さい。

  • centernode に親ノードが存在する場合は、親ノードを centernode に代入する
  • 親ノードが存在しない場合は何もしない
  • update_gui を呼び出して、Figure の描画を更新する

従って、このイベントハンドラは下記のプログラムで記述できます。

イベントハンドラの名前は、Marubatsu_GUI のボタンのイベントハンドラと同様に、ボタンのウィジェットを代入する属性の名前の前後に on__clicked を加えたものにしました。仮引数 b=None は、イベントループから呼び出される場合と、キー入力で呼び出される場合の両方に対応できるようにするためのものです。詳細は以前の記事を参照して下さい。

def on_left_button_clicked(b=None):
    if self.centernode.parent is not None:
        self.centernode = self.centernode.parent
        self.update_gui()

子ノードへの移動

→ ボタンを押した時に行う子のノードへの移動は、以下の手順で行うことができます。

  • centernode に子ノードが存在する場合は、最初の子ノードを centernode に代入する
  • 子ノードが存在しない場合は何もしない
  • update_gui を呼び出して、Figure の描画を更新する

従って、このイベントハンドラは下記のプログラムで記述できます。子ノードが存在するか どうかは、親ノードの children 属性の要素の数が 0 より大きいか どうかで判定できます。

def on_right_button_clicked(b=None):
    if len(self.centernode.children) > 0:
        self.centernode = self.centernode.children[0]
        self.update_gui()

親ノードと子ノードへの移動に対応する create_event_handler の定義

まだ、兄弟ノードへの移動に対応するイベントハンドラを定義していませんが、親ノードと子ノードへの移動ができれば、Mbtree_GUI の部分木の表示を変更できるようになるので、親ノードと子ノードへの移動に対応する create_event_handler を、下記のプログラムのように定義する事にします。

プログラムは、新しい機能を実装した際に、なるべくその都度正しく実装できているかどうかをチェックするべきです。そうしないと、バグが発生した場合に、どこでバグが発生したかを見つけることが困難になるからです。

  • 2 ~ 10 行目:上記のイベントハンドラを定義する
  • 12、13 行目:イベントハンドラとボタンを結び付ける
  • 15 ~ 24 行目:キー入力とイベントハンドラを結び付ける。詳細は以前の記事を参照
 1  def create_event_handler(self):
 2      def on_left_button_clicked(b=None):
 3          if self.centernode.parent is not None:
 4              self.centernode = self.centernode.parent
 5              self.update_gui()
 6              
 7      def on_right_button_clicked(b=None):
 8          if len(self.centernode.children) > 0:
 9              self.centernode = self.centernode.children[0]
10              self.update_gui()
11            
12      self.left_button.on_click(on_left_button_clicked)
13      self.right_button.on_click(on_right_button_clicked)
14
15      def on_key_press(event):
16          keymap = {
17              "left": on_left_button_clicked,
18              "right": on_right_button_clicked,
19          }
20          if event.key in keymap:
21              keymap[event.key]()
22            
23      # fig の画像イベントハンドラを結び付ける
24      self.fig.canvas.mpl_connect("key_press_event", on_key_press)        
25
26  Mbtree_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    def on_left_button_clicked(b=None):
        if self.centernode.parent is not None:
            self.centernode = self.centernode.parent
            self.update_gui()
            
    def on_right_button_clicked(b=None):
        if len(self.centernode.children) > 0:
            self.centernode = self.centernode.children[0]
            self.update_gui()
            
    self.left_button.on_click(on_left_button_clicked)
    self.right_button.on_click(on_right_button_clicked)

    def on_key_press(event):
        keymap = {
            "left": on_left_button_clicked,
            "right": on_right_button_clicked,
        }
        if event.key in keymap:
            keymap[event.key]()
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)        

Mbtree_GUI.create_event_handler = create_event_handler

上記の修正後に、下記のプログラムを実行し、← と → ボタンまたは、左右のカーソルキーを押すと、部分木の中心となるノードが変化するようになることを確認して下さい。

mbtree_gui = Mbtree_GUI(mbtree)

なお、→ ボタンを何度も押すと、下記の画面が表示され、それ以上 → ボタンを押しても表示が変化しないようになります。その理由について少し考えてみて下さい。

その理由は、上図では、右上の 〇 が勝利した局面を中心とする部分木が描画されているからです。決着がついた局面には子ノードは存在しないので、右上のゲーム盤の右には何も表示されず、→ ボタンをクリックしても子ノードには移動しません。

現状の Mbtree_GUI には、このように、中心となるノードが何であるかがわかりづらいという欠点 があります。その欠点については次回の記事で修正します。

兄弟ノードへの移動に対応する create_event_handler の定義

↑ ボタンを押した時に行う、一つ前の兄弟ノードへの移動の処理は、親ノードや子ノードへの移動のように簡単ではありません。その方法について少し考えてみて下さい。

一つ前の兄弟ノードへの移動の処理は、以下の手順で行うことができます。

  1. centernode に親ノードが存在しない場合は、兄弟ノードは存在しないので何もしない
  2. 親ノードの子ノードの一覧を表す centernode.parent.childnode 属性に代入された list の中で、centernode が何番のインデックスに代入されているかを調べる
  3. そのインデックスが 0 の場合は、一つ前の兄弟ノードは存在しないので何もしない
  4. 0 以外の場合は、一つ前のインデックスの要素を childnode に代入する
  5. update_gui を呼び出して、Figure の描画を更新する

上記の処理を行うイベントハンドラは、下記のプログラムで記述できます。

  • 2 行目:親ノードが存在する場合のみ処理を行うようにする
  • 3 行目:親ノードの子ノードの list の中で、centernode が代入されている要素のインデックスを、以前の記事で説明した index メソッドを使って計算する
  • 4、5 行目index が 0 以上の場合は、一つ前のインデックスを centernode とする
1  def on_up_button_clicked(b=None):
2      if self.centernode.parent is not None:
3          index = self.centernode.parent.children.index(self.centernode)
4          if index > 0:
5              self.centernode = self.centernode.parent.children[index - 1]
6              self.update_gui()

同様に、一つ後の兄弟ノードへの移動は、上記の手順 3、4 を下記のように修正することで行うことができます。

3. そのインデックスが最後のインデックスの場合は、一つ後の兄弟ノードは存在しないので何もしない
4. それ以外の場合は、一つ後のインデックスの要素を childnode に代入する

上記の処理を行うイベントハンドラは、下記のプログラムで記述できます。

  • 4 行目centernode が、親ノードの子ノードの最後の要素でないことは、list 最後の要素を表す [-1]is not 演算子を利用することで判定できる
1  def on_down_button_clicked(b=None):
2      if self.centernode.parent is not None:
3          index = self.centernode.parent.children.index(self.centernode)
4          if self.centernode.parent.children[-1] is not self.centernode:
5              self.centernode = self.centernode.parent.children[index + 1]
6              self.update_gui()

最後の要素に代入されているということは、その要素のインデックスが list の要素の数 - 1 であるということです。従って、上記の 4 行目は、下記のように親ノードの子ノードの数を使った式で記述することもできます。

if index != len(self.centernode.parent.children) - 1:

下記は、上記のイベントハンドラを組み込んだ create_event_handler のプログラムです。先程とほぼ同様なので、説明は省略します。

def create_event_handler(self):
    def on_left_button_clicked(b=None):
        if self.centernode.parent is not None:
            self.centernode = self.centernode.parent
            self.update_gui()
            
    def on_right_button_clicked(b=None):
        if len(self.centernode.children) > 0:
            self.centernode = self.centernode.children[0]
            self.update_gui()

    def on_up_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if index > 0:
                self.centernode = self.centernode.parent.children[index - 1]
                self.update_gui()
            
    def on_down_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if self.centernode.parent.children[-1] is not self.centernode:
                self.centernode = self.centernode.parent.children[index + 1]
                self.update_gui()            
            
    self.left_button.on_click(on_left_button_clicked)
    self.right_button.on_click(on_right_button_clicked)
    self.up_button.on_click(on_up_button_clicked)
    self.down_button.on_click(on_down_button_clicked)

    def on_key_press(event):
        keymap = {
            "left": on_left_button_clicked,
            "right": on_right_button_clicked,
            "up": on_up_button_clicked,
            "down": on_down_button_clicked,
        }
        if event.key in keymap:
            keymap[event.key]()
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)        

Mbtree_GUI.create_event_handler = create_event_handler

上記の修正後に、下記のプログラムを実行し、←、→、↑、↓ ボタンまたは、カーソルキーを押すと、部分木の中心となるノードが変化するようになることを確認して下さい。

mbtree_gui = Mbtree_GUI(mbtree)

また、下図のように、最も縦幅が必要になる、深さ 1 のノードを中心とする部分木が、Figure の中にすべて表示されることも確認して下さい。

今回の記事のまとめ

今回の記事では、ゲーム木を表示する GUI を作成し、4 つのボタンで描画する部分木の中心となるノードを移動できるようにしました。ただし、現状では中心となるノードが何であるかが分かりにくいなどの問題があります。次回の記事ではそれらの問題を修正するので、どのような問題があるかについて考えておいてください。

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

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

以下のリンクは、今回の記事で更新した tree.py です。

以下のリンクは、今回の記事で更新した gui.py です。

次回の記事

  1. ご存じの方がいらっしゃれば、コメントに書いていただければ嬉しいです

  2. メソッドの名前が subplots のように、複数形になっているのはそのためです

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