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を一から作成する その97 ゲーム木の生成過程を表示するアニメーション

Last updated at Posted at 2024-07-11

目次と前回の記事

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

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

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

ルールベースの AI の一覧

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

マウス操作による中心となるノードを移動する GUI の問題点と改良

前回の記事で実装した、マウス操作による、中心となるノードの移動には、問題があることがわかりましたので修正します。実際に操作してみればわかる問題点だと思いますので、どのような問題があるかについて少し考えてみて下さい。

問題点

以前の記事で説明したように、深さ 6 のノードを中心とする部分木 は、最も深い深さ 9 のノードまでを描画することができる ので、GUI の操作によって 深さが 7 以上のノードが中心とならない ようにする処理を行いました。

→ ボタンで中心となるノードを子ノードに移動する場合は、中心となるノードの 深さが 1 ずつしか深くならない ので、深さ 6 のノードが中心の場合に → ボタンを押した際に移動できなくしても特に問題はありませんでした。

一方、マウスによって中心となるノードを移動 する場合は、2 つ以上深いノードに移動 することができてしまうので、深さが 7 以上のノードへ移動できなくする という処理には 問題があります。言葉の説明では意味がわからないと思いますので、具体例を示します。

下図は、深さ 5 のノードを中心とする部分木です。下図で、一番右の列に表示される、深さが 7 のノードの上でマウスを押しても 中心となるノードは移動しません。そのため、下図に表示されている 深さが 7 のノードの子ノードが表示される部分木 を表示するためには、深さが 6 のノードの上でマウスを押す必要があります

具体的には、上図の深さが 7 の赤丸のノードの上でマウスを押しても表示は更新されず、赤丸の子ノードが表示される部分木を表示するためには、深さが 6 の青丸のノードの上でマウスを押す必要があります。下図は、上図で青丸のノードをマウスで押した場合の図で、赤丸のノードの子ノードが表示された部分木が表示されています。

赤丸の子ノードを表示するため に、赤丸以外 の青丸の ノードをマウスで押さなければならない というのは、わかりづらく不便 です。

この問題を修正する方法について少し考えてみて下さい。

問題の修正方法

以下は、この問題を修正する方法の一つです。他にも良い方法があるかもしれないので、もっと良い方法を思いついた方はその方法を実装してみて下さい。

  • 部分木の中で、赤い枠を表示するノード を、選択されたノード と表現することにする
  • すべてのノード を GUI の操作で 選択できる ようにする
  • 選択されたノードの 深さが 6 以下 の場合は、これまでと同様 に、選択されたノードを中心とした部分木を描画する
  • 選択されたノードの 深さが 7 以上 の場合は、その親ノードを辿った、深さが 6 の親ノードを中心とする部分木を描画 する

上記のように修正することで、深さが 7 以上のノードが選択された場合でも、深さが 6 から 9 までの部分木が描画されるようになります1

わかりづらいかもしれませんが、この修正で部分木の 中心となるノードがなくなるわけではありません。また、選択されたノードの 深さが 6 以下の場合 は選択されたノードと中心となるノードは 同じになります。この修正は、GUI の 操作で中心となるノードではなく、赤枠で囲われた 選択されたノードを操作 できるようにするというものです

なお、この修正は、今回の記事の後半で行う、ゲーム木の生成の過程のアニメーション処理を行う際にも必要となる修正です。

centernode 属性の名前の修正

赤枠で表示されるノードは、これまでは中心となるノードと呼んでいたので Mbtree_GUI クラスの centernode 属性に代入していましたが、選択された(selected)ノード と呼び方を変えることにしたので、代入する 属性の名前selectednode に変更することにします。

下記は、そのように Mbtree_GUI クラスを修正したプログラムです。なお、修正は VSCode の シンボル名の変更の機能を使えば簡単なので、変更場所の説明は省略します。

修正したプログラム(長いのでクリックして開いてください)
from gui import GUI
import matplotlib.pyplot as plt
import ipywidgets as widgets

class Mbtree_GUI(GUI):
    def __init__(self, mbtree, size=0.15):
        self.mbtree = mbtree
        self.size = size
        self.width = 50
        self.height = 64
        self.selectednode = self.mbtree.root
        super().__init__()
        
    def create_widgets(self):
        self.output = widgets.Output()  
        self.left_button = self.create_button("", 50)
        self.up_button = self.create_button("", 50)
        self.right_button = self.create_button("", 50)
        self.down_button = self.create_button("", 50)
        self.help_button = self.create_button("", 50)
        self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
        
        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):
        def on_left_button_clicked(b=None):
            if self.selectednode.parent is not None:
                self.selectednode = self.selectednode.parent
                self.update_gui()
                
        def on_right_button_clicked(b=None):
            if self.selectednode.depth < 6 and len(self.selectednode.children) > 0:
                self.selectednode = self.selectednode.children[0]
                self.update_gui()

        def on_up_button_clicked(b=None):
            if self.selectednode.parent is not None:
                index = self.selectednode.parent.children.index(self.selectednode)
                if index > 0:
                    self.selectednode = self.selectednode.parent.children[index - 1]
                    self.update_gui()
                
        def on_down_button_clicked(b=None):
            if self.selectednode.parent is not None:
                index = self.selectednode.parent.children.index(self.selectednode)
                if self.selectednode.parent.children[-1] is not self.selectednode:
                    self.selectednode = self.selectednode.parent.children[index + 1]
                    self.update_gui()            
                    
        def on_help_button_clicked(b=None):
            self.output.clear_output()
            with self.output:
                print("""操作説明

    下記のキーとボタンで中心となるノードを移動できる。ただし、深さが 7 以上のノードへは移動できない
    ←、0 キー:親ノードへ移動
    ↑:一つ前の兄弟ノードへ移動
    ↓:一つ後の兄弟ノードへ移動
    →:先頭の子ノードへ移動

    テンキーで、対応するマスに着手が行われた子ノードへ移動する
    ノードの上でマウスを押すことでそのノードへ移動する
    """)
                            
        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)
        self.help_button.on_click(on_help_button_clicked)

        def on_key_press(event):
            keymap = {
                "left": on_left_button_clicked,
                "0": 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]()
            elif self.selectednode.depth < 6:
                try:
                    num = int(event.key) - 1
                    x = num % 3
                    y = 2 - (num // 3)
                    move = (x, y)
                    if move in self.selectednode.children_by_move:
                        self.selectednode = self.selectednode.children_by_move[move]
                        self.update_gui()
                except:
                    pass            
                
        def on_mouse_down(event):
            for rect, node in self.mbtree.nodes_by_rect.items():
                if node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
                    self.selectednode = node
                    self.update_gui()
                    break               
                
        # fig の画像イベントハンドラを結び付ける
        self.fig.canvas.mpl_connect("key_press_event", on_key_press)
        self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    
            
    def display_widgets(self):
        hbox1 = widgets.HBox([self.label, self.up_button, self.label])
        hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
                            self.label, self.help_button])
        hbox3 = widgets.HBox([self.label, self.down_button, self.label])
        display(widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas]))   

    def update_gui(self):
        self.ax.clear()
        self.ax.set_xlim(-1, self.width - 1)
        self.ax.set_ylim(0, self.height)   
        self.ax.invert_yaxis()
        self.ax.axis("off")   
        
        if self.selectednode.depth <= 4:
            maxdepth = self.selectednode.depth + 1
        elif self.selectednode.depth == 5:
            maxdepth = 7
        else:
            maxdepth = 9
        self.mbtree.draw_subtree(self.selectednode, ax=self.ax, maxdepth=maxdepth)
        
        disabled = self.selectednode.parent is None
        self.set_button_status(self.left_button, disabled=disabled)
        disabled = self.selectednode.depth >= 6 or len(self.selectednode.children) == 0
        self.set_button_status(self.right_button, disabled=disabled)
        disabled = self.selectednode.parent is None or self.selectednode.parent.children.index(self.selectednode) == 0
        self.set_button_status(self.up_button, disabled=disabled)
        disabled = self.selectednode.parent is None or self.selectednode.parent.children[-1] is self.selectednode
        self.set_button_status(self.down_button, disabled=disabled)

draw_subtree の修正

部分木の中で、中心となるノード と、赤い枠を表示する 選択されたノード異なるノードになる場合が生じた ので、draw_subtree には、中心となるノードと、選択されたノードを代入する 異なる仮引数を用意する必要 があります。また、draw_node を呼び出す際に 強調して表示するノードを選択されたノードに変更 する必要があります。

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

  • 3 行目:選択されたノードを代入する、デフォルト値を None とする仮引数 selectednode を追加する
  • 6 行目draw_node で強調して表示するノードを、selectednode に変更する
1  from tree import Mbtree
2  
3  def draw_subtree(self, centernode=None, selectednode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
4              else:
5                  dx = 5 * node.depth
6                  emphasize = node is selectednode
7                  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
8            
9  Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
from tree import Mbtree

def draw_subtree(self, centernode=None, selectednode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):   
    self.nodes_by_rect = {}

    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
                emphasize = node is selectednode
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                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
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        rect = parent.draw_node(ax, maxdepth=maxdepth, 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
            rect = node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
修正箇所
from tree import Mbtree

-def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):   
+def draw_subtree(self, centernode=None, selectednode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
             else:
                dx = 5 * node.depth
-               emphasize = node is centernode
+               emphasize = node is selectednode
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
            
Mbtree.draw_subtree = draw_subtree

上記の修正後に、下記のプログラムを実行することで、実行結果から draw_subtree中心となるノード と、選択されたノード異なる部分木を表示 できるようになったことを確認できます。

なお、下記のプログラムでは、下記のような部分木を表示しています。

  • ルートノードを中心とする、深さ 1 までの部分木
  • 選択されたノードは、深さ 1 の最初のノード
mbtree = Mbtree()
mbtree.draw_subtree(centernode=mbtree.root, 
                    selectednode=mbtree.nodelist_by_depth[1][0], maxdepth=1)

実行結果

create_eventhandler の修正

次に、create_eventhandler を下記のプログラムのように、深さ 7 以上のノードを選択状態にできるように修正します。

  • 3、23、27 行目:深さが 6 以下の場合に制限するという条件を、条件式から削除する
  • 11 行目:操作説明のメッセージを修正する
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_right_button_clicked(b=None):
 3          if len(self.selectednode.children) > 0:
 4              self.selectednode = self.selectednode.children[0]
 5              self.update_gui()
元と同じなので省略
 6      def on_help_button_clicked(b=None):
 7          self.output.clear_output()
 8          with self.output:
 9              print("""操作説明
10
11  下記のキーとボタンで中心となるノードを移動できる。
12  ←、0 キー:親ノードへ移動
13  ↑:一つ前の兄弟ノードへ移動
14  ↓:一つ後の兄弟ノードへ移動
15  →:先頭の子ノードへ移動
16
17  テンキーで、対応するマスに着手が行われた子ノードへ移動する
18  ノードの上でマウスを押すことでそのノードへ移動する
19  """)
元と同じなので省略
20      def on_key_press(event):
元と同じなので省略
21          if event.key in keymap:
22              keymap[event.key]()
23          else:
24              try:
元と同じなので省略
25      def on_mouse_down(event):
26          for rect, node in self.mbtree.nodes_by_rect.items():
27              if rect.is_inside(event.xdata, event.ydata):
28                  self.selectednode = node
29                  self.update_gui()
30                  break               
元と同じなので省略
31    
32  Mbtree_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    def on_left_button_clicked(b=None):
        if self.selectednode.parent is not None:
            self.selectednode = self.selectednode.parent
            self.update_gui()
            
    def on_right_button_clicked(b=None):
        if len(self.selectednode.children) > 0:
            self.selectednode = self.selectednode.children[0]
            self.update_gui()

    def on_up_button_clicked(b=None):
        if self.selectednode.parent is not None:
            index = self.selectednode.parent.children.index(self.selectednode)
            if index > 0:
                self.selectednode = self.selectednode.parent.children[index - 1]
                self.update_gui()
            
    def on_down_button_clicked(b=None):
        if self.selectednode.parent is not None:
            index = self.selectednode.parent.children.index(self.selectednode)
            if self.selectednode.parent.children[-1] is not self.selectednode:
                self.selectednode = self.selectednode.parent.children[index + 1]
                self.update_gui()            
                
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

下記のキーとボタンで中心となるノードを移動できる。
←、0 キー:親ノードへ移動
↑:一つ前の兄弟ノードへ移動
↓:一つ後の兄弟ノードへ移動
→:先頭の子ノードへ移動

テンキーで、対応するマスに着手が行われた子ノードへ移動する
ノードの上でマウスを押すことでそのノードへ移動する
""")
                        
    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)
    self.help_button.on_click(on_help_button_clicked)

    def on_key_press(event):
        keymap = {
            "left": on_left_button_clicked,
            "0": 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]()
        else:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.selectednode.children_by_move:
                    self.selectednode = self.selectednode.children_by_move[move]
                    self.update_gui()
            except:
                pass            
            
    def on_mouse_down(event):
        for rect, node in self.mbtree.nodes_by_rect.items():
            if rect.is_inside(event.xdata, event.ydata):
                self.selectednode = node
                self.update_gui()
                break               
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    
    
Mbtree_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
    def on_right_button_clicked(b=None):
-       if self.selectednode.depth < 6 and len(self.selectednode.children) > 0:
+       if len(self.selectednode.children) > 0:
            self.selectednode = self.selectednode.children[0]
            self.update_gui()
元と同じなので省略
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

-下記のキーとボタンで中心となるノードを移動できる。ただし、深さが 7 以上のノードへは移動できない
+下記のキーとボタンで中心となるノードを移動できる。
←、0 キー:親ノードへ移動
↑:一つ前の兄弟ノードへ移動
↓:一つ後の兄弟ノードへ移動
→:先頭の子ノードへ移動

テンキーで、対応するマスに着手が行われた子ノードへ移動する
ノードの上でマウスを押すことでそのノードへ移動する
""")
元と同じなので省略
    def on_key_press(event):
元と同じなので省略
        if event.key in keymap:
            keymap[event.key]()
-       elif self.selectednode.depth < 6:
+       else:
            try:
元と同じなので省略
    def on_mouse_down(event):
        for rect, node in self.mbtree.nodes_by_rect.items():
-           if node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
+           if rect.is_inside(event.xdata, event.ydata):
                self.selectednode = node
                self.update_gui()
                break               
元と同じなので省略
    
Mbtree_GUI.create_event_handler = create_event_handler

上記の修正後に、下記のプログラムを実行すると、実行結果のように赤い枠のノードが表示されなくなります。これは、draw_subtree を先程修正したにも関わらず、draw_subtree を呼び出す処理を修正していないからです。

mbtree_gui = Mbtree_GUI(mbtree)

実行結果

update_gui の修正

draw_subtree は、update_gui から呼び出しているので、update_gui の中で、下記のような 中心となるノードを計算する必要 があります。

  • 選択されたノードの 深さが 6 以下 の場合は、これまでと同様 に、選択されたノードを中心 とした部分木を描画する
  • 選択されたノードの 深さが 7 以上 の場合は、その親ノードを辿った、深さが 6 の親ノードを中心 とする部分木を描画 する

そのような中心となるノードは、下記のプログラムの 2 ~ 4 行目の処理で計算できます。

  • 2 行目centernode に選択されたノードを代入する
  • 3、4 行目centernode の深さが 6 より大きい場合に、centernode に親ノードを代入する繰り返しの処理を行う
  • 5 行目:計算された centernode を実引数に記述して draw_subtree を呼び出す

上記の処理を行うと、centernode に下記のようなノードが代入されます。

  • 選択されたノードの深さが 6 以下の場合は、3 行目の条件式が 最初から False になる ため 4 行目の処理が行われない。従って、centernode には選択されたノードが代入 される
  • 選択されたノードの深さが 7 以上の場合は centernode の深さが 6 になるまで 4 行目の処理が繰り返し行われる。そのため、centernode には、深さが 6 の選択されたノードの親ノードが代入される
1  def update_gui(self):
元と同じなので省略
2      centernode = self.selectednode
3      while centernode.depth > 6:
4          centernode = centernode.parent
5      self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略    
6    
7  Mbtree_GUI.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.invert_yaxis()
    self.ax.axis("off")   
    
    if self.selectednode.depth <= 4:
        maxdepth = self.selectednode.depth + 1
    elif self.selectednode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    centernode = self.selectednode
    while centernode.depth > 6:
        centernode = centernode.parent
    self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
    
    disabled = self.selectednode.parent is None
    self.set_button_status(self.left_button, disabled=disabled)
    disabled = self.selectednode.depth >= 6 or len(self.selectednode.children) == 0
    self.set_button_status(self.right_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children.index(self.selectednode) == 0
    self.set_button_status(self.up_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children[-1] is self.selectednode
    self.set_button_status(self.down_button, disabled=disabled)
    
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
+   centernode = self.selectednode
+   while centernode.depth > 6:
+       centernode = centernode.parent
-   self.mbtree.draw_subtree(self.selectednode, ax=self.ax, maxdepth=maxdepth)
+   self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略    
    
Mbtree_GUI.update_gui = update_gui

上記の修正後に、下記のプログラムを実行して深さ 7 以上のノードを選択状態にすると、実行結果のように、そのノードが赤い枠で表示され、そのノードの深さが 6 の親ノードが中心となる部分木が表示されるようになります。

mbtree_gui = Mbtree_GUI(mbtree)

実行結果

これで、ゲーム木を視覚化する GUI は完成ですが、他にも機能を付け加えたい人は実装してみて下さい。

ゲーム木の作成の過程の視覚化

Mbtree クラスでは、幅優先アルゴリズムによって、ゲーム木の作成を行いました。先程完成したゲーム木を視覚化する GUI では、完成したゲーム木を視覚化 することができますが、ゲーム木が どのように作成されたか を知ることはできません。そこで、幅優先アルゴリズムによって、ゲーム木のノードがどのような順番で作成されるかを視覚化することにします。実は、本記事で、ここまで部分木の視覚化の実装を行ってきた一つの理由は、ゲーム木の作成の過程の視覚化を行いたかったからです。

どのような方法で視覚化すればよいかについて少し考えてみて下さい。

〇×ゲームのリプレイ機能と同様の方法による視覚化

視覚化の方法として、〇×ゲームの GUI のリプレイ機能のように、下図の <<、<、>、>> の 4 つのボタンと IntSlider のウィジェットで、ゲーム木が作成される過程を表示するという方法が考えられますが、この方法はあまり良い方法ではないでしょう。

その理由の一つは、上記の UI では、ゲーム木が作成される過程の 表示を進めるため には、> ボタンをクリックする必要がある からです。〇× ゲームのゲーム木のノードは、以前の記事で説明したように、50 万以上もある ため、> ボタンをクリックして表示を進めるのは 現実的ではない でしょう。

また、他の理由としては、〇×ゲームのリプレイ機能の場合は、ゲームの経過を 1 手ずつ前後に移動しながら確認したい ので上記のような UI を採用しましたが、ゲーム木が作成される過程を表示する際に < ボタンをクリックして 1 つ前に戻る必要はあまりない でしょう。

ゲーム木が作成される過程を表示する場合は、自動的に表示が更新 されるという、アニメーション(動画)によって行うのが自然 だと思いますので、本記事では アニメーションによるゲーム木の生成の過程を表示する方法 について説明します。〇× ゲームのリプレイと同じ UI が良いと思った方は、そちらを採用して下さい。

Play ウィジェットによるアニメーション

アニメーションは、一定時間おき に、少しずつ内容が異なる画像を表示する ことで行います。原理的には、パラパラ漫画と同じ です。ノートや教科書の端に、パラパラ漫画によるアニメーションを描いたことがある人は多いのではないでしょうか?

一定時間おきに表示する画像 のことを フレーム(frame) と呼び、1 秒間に表示するフレームの数fps(frame per second)と呼びます。fps が高いほうが滑らかなアニメーションになり、テレビ番組や映画などでは約 30 fps、ゲームでは 60 fps が一般的なようです。

従って、プログラムでアニメーションを行うためには、一定時間おきに表示を更新する処理を行う 必要があります。

Play ウィジェット

ipywidgets には、アニメーションを行うために利用できる Play というウィジェットが用意されています。Play ウィジェットは、下図のような表示が行われるウィジェットで、下記のように、一定時間おきvalue 属性を変化させる機能 を持ちます。

  • 上図のような、3 つのボタンで構成されるウィジェットである
  • 三角形のボタンをクリックすると、一定時間おきに、Play ウィジェットの value 属性の値が変化 するようになる。その後、もう一度そのボタンをクリックすると value 属性の値が変化しなくなる
  • value 属性には、初期値最小値最大値加算される値 を設定することができる
  • 四角形のボタンをクリックすると、value 属性の値が最小値になり、時間経過で変化しないようになる
  • 一番右のボタンをクリックして ON の状態にすると、value 属性の値が最大値を超えた場合に初期値に戻り、ループするようになる

Play ウィジェットの詳細については、下記のリンク先を参照して下さい。

Play ウィジェットの属性

Play ウィジェットには、主に下記の属性があります。

属性 意味 デフォルト値
value 一定時間おきに値が変化する属性 0
min value 属性の最小値 0
max value 属性の最大値 100
step 時間経過による value 属性の変化量 1
interval value 属性が変化する間隔。単位はミリ秒 100
disabled True の場合にウィジェットを操作できなくなる False

Play ウィジェットによるアニメーション処理

Play ウィジェットは、他のウィジェットと同様に observe メソッドを使ってその 属性が変化した際に実行するイベントハンドラを結び付ける ことができ、その イベントハンドラに画像の表示を更新する処理を記述する ことで、アニメーションを行うことができます。

アニメーションの fps は、1 秒間に表示する画像の数なので、「1000 / interval 属性の値」という式で計算できます。例えば interval 属性に 100 を代入 した場合のアニメーションは、1000 / 100 = 10 fps のアニメーションになります。

逆に、fps から interval 属性の値を求める 場合は、「1000 / fps」 という式で計算できます。例えば 20 fps のアニメーションは、inteval 属性に 1000 / 20 = 50 を設定 します

下記は、赤い正方形の辺の長さを 1 から 10 まで増やしながら表示する というアニメーションを、10 fps で行うプログラムです。実行すると、実行結果の左図のような表示が行われ、三角形のボタンをクリックすると、右図のようなアニメーションが表示されます。

なお、イベントハンドラは value 属性が変更された際に呼び出され、value 属性の値 は Play ウィジェットの 三角形のボタンをクリックするまでは変化しない ので、下記のプログラムを 実行した直後 は、Figure には何も表示されません

  • 3 ~ 5 行目:ヘッダ2の無い Figure を作成し、Axes の軸を表示しないようにする
  • 7 行目value 属性の初期値が 1、最小値が 1、最大値が 10、fps が 10 になるように interval 属性が 100 の Play ウィジェットを作成し play に代入する
  • 9 ~ 17 行目:Play ウィジェットの value 属性が変化した際に呼び出されるイベントハンドラを定義する
  • 10 ~ 13 行目:それまでに Axes に表示されていた内容をクリアし、Axes の表示範囲を設定し、軸を表示しないようにする
  • 15 行目:更新された Play ウィジェットの value 属性の値をローカル変数 value に代入する。なお、この部分には value = play.value を記述しても良い
  • 16、17 行目:(0, 0) を頂点とし、縦横の辺の長さが value の赤い正方形の Artist を作成し、Axes に登録して表示する
  • 19 行目:Play ウィジェットに、value 属性の値が変更された際に呼び出されるイベントハンドラを結び付ける
 1  import matplotlib.patches as patches
 2
 3  fig, ax = plt.subplots(figsize=(3, 3))
 4  fig.canvas.header_visible = False
 5  ax.axis("off")
 6
 7  play = widgets.Play(value=1, min=1, max=10, intervale=100)
 8
 9  def on_play_changed(changed):
10      ax.clear()
11      ax.set_xlim(0, 10)
12      ax.set_ylim(0, 10)
13      ax.axis("off")
14    
15      value = changed["new"]    
16      rect = patches.Rectangle(xy=(0, 0), width=value, height=value, fc="red")
17      ax.add_artist(rect)
18    
19  play.observe(on_play_changed, names="value")
20
21  display(play)    
行番号のないプログラム
import matplotlib.patches as patches

fig, ax = plt.subplots(figsize=(3, 3))
fig.canvas.header_visible = False
a x.axis("off")

play = widgets.Play(value=1, min=1, max=10, intervale=100)

def on_play_changed(changed):
    ax.clear()
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 10)
    ax.axis("off")
    
    value = changed["new"]    
    rect = patches.Rectangle(xy=(0, 0), width=value, height=value, fc="red")
    ax.add_artist(rect)
    
play.observe(on_play_changed, names="value")

display(play)    

実行結果

 

上記のプログラムを実行して、Play ウィジェットの三角形のボタンをクリックすると、下記のような処理が行われるため、上図右のようなアニメーションが行われます。なお、上図右では、アニメーションがリピートしますが、実際に表示されるアニメーション では、三角形のボタンを押しただけでは アニメーションのリピートは行われません。Play ウィジェットの右のボタンをクリックしてから三角形のボタンをクリックすることでアニメーションがリピートするようになります。

  • play.value が 100 ミリ秒おきに 1 増えるようになる。
  • value 属性の値が変更されると、on_play_changed が呼び出される
  • on_play_changed では、value 属性の値を辺の長さとする赤い正方形を描画する
  • 上記から、1000 / 100 = 10 fps のアニメーションが表示される

matplotlib を利用したアニメーションは、matplotlib の ArtistAnimationFuncAnimation を使って作成することもできます。ただし、これらを使ってアニメーションを作成する場合は、アニメーションの すべてのフレームの画像を用意する必要 があります。〇×ゲームのゲーム木には 50 万以上のノードが存在するので、〇×ゲームのゲーム木の生成過程を表すアニメーションを作成するためには、50 万以上のフレームの画像を作成する必要があるため、アニメーションを開始する前 に、かなりの時間が必要 になります。そのため、本記事ではそれらは採用しません。

なお、ArtistAnimation などには、アニメーションの作成に時間がかかるという欠点がありますが、動画ファイルとして保存することができるなどの利点があります。

matplotlib の ArtistAnimation や FuncAnimation を使ったアニメーションに興味がある方は、下記のリンク先を参照して下さい。本記事でも、必要があればそれらの使い方を今後紹介するかもしれません。

具体的な手順は長くなるので説明しませんが、本記事の中で先程表示したアニメーションの画像は、FuncAnimation を使って作成した mp4 の動画を、アニメーション GIF の形式に変換したものです。アニメーション GIF に変換した理由は、Quiita には、mp4 の動画ファイルをアップロードすることができないからです。

なお、ArtistAnimation や FuncAnimation は、Figure のアニメーションを作成するためのものなので、Play ウィジェットや IntSlider ウィジェットなどをアニメーションに含めることはできません。以後の記事のアニメーション画像に Play ウィジェットなどを表示しないのはそのためです。

Play ウィジェットと IntSlider ウィジェットのリンク

Play ウィジェットだけ では、アニメーションの どのフレームが現在描画されているかわからない という問題があります。この問題を解決する方法の一つに、Play ウィジェットIntSlider ウィジェットリンク して連動させるというものがあります。

ipywidgets のウィジェットは、jslink という関数によって、複数のウィジェットの属性をリンクして連動する ことができます。具体的には、下記のプログラムのように記述することで、リンクさせた片方のウィジェットの 属性の値が変更される と、自動的にもう片方の属性の値がその値に変更される ようになります。

widgets.jslink((ウィジェット1, 属性名1), (ウィジェット2, 属性名1))

下記は、先程のプログラムに IntSlider を加え、Play ウィジェットと IntSlider の value 属性をリンク して連動させるようにしたものです。

  • 3 行目:Play と同じ valueminmax 属性の値を設定した IntSlider を作成する
  • 4 行目:Play と IntSlider の value 属性をリンクする
  • 7 行目:Play と IntSlider を HBox を使って横に並べて配置して表示する
元と同じなので省略
1
2  play = widgets.Play(value=1, min=1, max=10, interval=100)
3  slider = widgets.IntSlider(value=1, min=1, max=10)
4  widgets.jslink((play, "value"), (slider, "value"))
5
元と同じなので省略
6
7  display(widgets.HBox([play, slider]))    
行番号のないプログラム
fig, ax = plt.subplots(figsize=(3, 3))
fig.canvas.header_visible = False
ax.axis("off")

play = widgets.Play(value=1, min=1, max=10, interval=100)
slider = widgets.IntSlider(value=1, min=1, max=10)
widgets.jslink((play, "value"), (slider, "value"))

def on_play_changed(changed):
    ax.clear()
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 10)
    ax.axis("off")
    
    value = changed["new"]    
    rect = patches.Rectangle(xy=(0, 0), width=value, height=value, fc="red")
    ax.add_artist(rect)
    
play.observe(on_play_changed, names="value")

display(widgets.HBox([play, slider]))    
修正箇所
元と同じなので省略

play = widgets.Play(value=1, min=1, max=10, interval=100)
+slider = widgets.IntSlider(value=1, min=1, max=10)
+widgets.jslink((play, "value"), (slider, "value"))

元と同じなので省略

-display(play)    
+display(widgets.HBox([play, slider]))    

実行結果(Figure の部分は省略します)

上記のプログラムを実行すると、下図のように Play ウィジェットの右に IntSlider が表示されるようになます。また、Play ウィジェットと IntSlider ウィジェットが下記のように連動するようになります。

  • Play ウィジェットの三角形のボタンをクリックすると、IntSlider が Play ウィジェットの value 属性の値に連動して変化するようになる
  • IntSlider をドラッグして変更すると、Play ウィジェットの value 属性の値が連動して変更されてイベントハンドラが呼び出されるようになり、Figure の表示が更新される

さらに、< ボタンや > ボタンを加えることで、〇×ゲームのリプレイ機能のように、アニメーションのフレームを 1 つずつ前後にずらすことができるようになります。

jslink による属性のリンクは、イベントハンドラの仕組みを使って行うこともできます。例えば、Play の value 属性が変更された際に呼び出されるイベントハンドラと、Slider の value 属性が変更された際に呼び出されるイベントハンドラに下記のように記述します。なお、イベントハンドラとウィジェットの結び付けは省略します。

def on_play_changed(changed):
    slider.value = changed["new"]

def on_slider_changed(changed):
    play.value = changed["new"]

ただし、イベントハンドラの仕組みを使った連動には、以下のような欠点があるので、属性の値を連動するだけ の場合は、jslink を利用したほうが良い でしょう。

  • イベントループからイベントハンドラが呼び出されるという仕組みから、連動が瞬時に行われず、ほんの少し時間が経過してから連動が行われる
  • jslink による属性のリンクは、イベントハンドラの仕組みを利用していないので、属性の値が変更された瞬間にもう片方の属性の値が変更される
  • jslink による属性のリンクは、2 つのイベントハンドラを定義する必要がなく、1 行で簡潔に記述できる

ただし、jslink で行うことができるのは、属性の値の連動だけ です。属性の値が変更した際に、画像の表示などの、属性の値の連動以外の処理を行いたい場合は、イベントハンドラを利用する必要がある点に注意して下さい。

jslink は、2 つの属性を双方向にリンクしますが、片方向のみのリンクを行う jsdlink3という関数もあります。

双方向と片方向の違いがわらない人がいるかもしれないので、具体例を挙げます。

双方向のリンクでは、属性 A と B があった時に、以下のような連動が行われます。

  • 属性 A が変化した場合に、属性 B の値が変化する
  • 属性 B が変化した場合に、属性 A の値が変化する

片方向のリンクでは、属性 A と B があった時に、以下のような連動が行われます。

  • 属性 A が変化した場合に、属性 B の値が変化する
  • 属性 B が変化した場合に、属性 A の値は変化しない

なお、イベントハンドラの仕組みで属性のリンクを記述する場合は、片方のウィジェットのイベントハンドラのみを記述することで片方向のリンクになります。

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

アニメーションの仕様

アニメーションを行う方法がわかったので、ゲーム木の生成の過程の アニメーションの仕様を決める ことにします。どのような仕様が良いかについて少し考えてみて下さい。

一つの方法として、ルートノードのみのゲーム木から始め、Play ウィジェットの value 属性の値が 1 増える ごとに、幅優先アルゴリズムと同じ順番で ゲーム木にノードを追加 し、それを表示するという方法が考えられるでしょう。ただし、本記事ではこの方法は、下記の理由から採用しないことにします。

  • 上記のアニメーションを行うためには、value 属性の値が 1 増えた際に、ゲーム木に対して、幅優先アルゴリズムと同じ順番で、ノードを 1 つだけ追加するという処理 を行う必要がある。そのような処理を、これまでに説明した方法で行うのは困難である
  • 上記の方法でアニメーションを行った場合に、IntSlider の値を大きく増やす と、処理に時間がかかってしまう。例えば、IntSlider の値を 0 から 約 50 万 に増やした場合、50 万個のノードをゲーム木に追加 する必要があるため、かなりの時間が必要 になる。このことは、ゲーム木を作成する際に 30 秒以上の時間が必要になることから明らかである
  • IntSlider の値を減らした場合 に、ゲーム木を 一から作り直す 必要が生じる

もう一つの方法としては、完成したゲーム木 に対して、Play ウィジェットの value 属性の値が 1 増えるごとに、幅優先アルゴリズムと 同じ順番 でゲーム木に 追加したノード を、赤い枠で選択状態にして表示する という方法が考えられます。具体的には、下記のような方法でアニメーションを表示します。

  • アニメーションのフレームを 0 番から数えることにする

  • 0 番のフレームには、ルートノードが選択状態になった部分木を描画する

  • n 番のフレームには、ゲーム木に n 番目に追加されたノードが中心かつ、選択状態になった部分木を描画する

このアニメーションでは、表示される 部分木の赤い枠のノードを見る ことで、どのような順番でゲーム木にノードが追加されていくか を知ることができます。

また、この方法では、ゲーム木を 新しく作り直す必要はない ので、IntSlider でアニメーションのフレームを大きく増やしても、表示に大きな時間がかかるようなことはありません。

ただし、上記の方法で本当にわかりやすいアニメーションが表示できるという保証はありませんので、上記の方針でアニメーションを実装した結果、わかりづらいことが判明した時点で、必要に応じて仕様を修正することにします。

Mbtree クラスの修正

上記のようなアニメーションを行うためには、ゲーム木の 各ノード が、ゲーム木に追加された順番を記録 する必要があります。そのような情報は、list の要素 に、ゲーム木に 追加された順番でノードを代入 することで表現することができます。

そこで、Mbtree クラスに、その情報を代入する nodelist という属性を追加することにします。そうすることで、Play ウィジェットの value 属性 の値を nodelist のインデックス とすることで、value 番のフレーム に表示する部分木の 選択されたノードを計算 することができるようになります。

幅優先探索では、深さが浅い順にノードを追加 し、各深さのノードの情報 は、ゲーム木に 追加された順番 で Mbtree クラスの nodelist_by_depth 属性に代入 されています。そのため、nodelist は、下記のプログラムのように記述することで計算できます。

  • 5 行目nodelist 属性を空の list で初期化する
  • 8 行目nodelist に、深さが浅い順に nodelist_by_depth に記録されたノードの list を += 演算子で 拡張 する
 1  from marubatsu import Marubatsu
 2
 3  def create_tree_by_bf(self):
元と同じなので省略
 4      self.nodenum = 0
 5      self.nodelist = []
 6      for nodelist in self.nodelist_by_depth:
 7          self.nodenum += len(nodelist)
 8          self.nodelist += nodelist
 9      print(f"total node num = {self.nodenum}")
10    
11  Mbtree.create_tree_by_bf = create_tree_by_bf
行番号のないプログラム
from marubatsu import Marubatsu

def create_tree_by_bf(self):
    # 深さ 0 のノードを、子ノードを作成するノードのリストに登録する
    nodelist = [self.root]
    depth = 0
    # 各深さのノードのリストを記録する変数を初期化する
    self.nodelist_by_depth = [ nodelist ]
    
    # 深さ depth のノードのリストが空になるまで繰り返し処理を行う
    while len(nodelist) > 0:
        childnodelist = [] 
        for node in nodelist:
            if node.mb.status == Marubatsu.PLAYING:
                node.calc_children()
                childnodelist += node.children
        self.nodelist_by_depth.append(childnodelist)
        nodelist = childnodelist
        depth += 1
        print(f"{len(nodelist):>6} depth {depth} node created")
        
    self.nodenum = 0
    self.nodelist = []
    for nodelist in self.nodelist_by_depth:
        self.nodenum += len(nodelist)
        self.nodelist += nodelist
    print(f"total node num = {self.nodenum}")
    
Mbtree.create_tree_by_bf = create_tree_by_bf
修正箇所
from marubatsu import Marubatsu

def create_tree_by_bf(self):
元と同じなので省略
    self.nodenum = 0
+   self.nodelist = []
    for nodelist in self.nodelist_by_depth:
        self.nodenum += len(nodelist)
+       self.nodelist += nodelist
    print(f"total node num = {self.nodenum}")
    
Mbtree.create_tree_by_bf = create_tree_by_bf

上記の修正後に、下記のプログラムで Mbtree クラスのインスタンスを作成しなおし、2 行目で nodelist 属性の要素の数 を表示すると、total node num の行で表示された値と同じ値が表示 されることが確認できます。また、3 行目から nodelist の先頭の要素 に、ルートノードが代入されている ことが確認できます。

mbtree = Mbtree()
print(len(mbtree.nodelist))
print(mbtree.nodelist[0] is mbtree.root)

実行結果

     9 depth 1 node created
    72 depth 2 node created
   504 depth 3 node created
  3024 depth 4 node created
 15120 depth 5 node created
 54720 depth 6 node created
148176 depth 7 node created
200448 depth 8 node created
127872 depth 9 node created
     0 depth 10 node created
total node num = 549946
549946
True

Mbtree_Anim クラスの定義

アニメーションの処理 は、Play ウィジェットや IntSlider ウィジェットを利用した GUI のプログラム なので、Mbtree_GUI クラスと同様に、GUI クラスを継承 した Mbtree_Anim というクラスを定義して実装することにします。

下記は Mbtree_Anim クラスの定義です。Mbtree_Anim では ゲーム木の部分木を描画する ので、__init__ メソッドには、下記のプログラムのように、Mbtree_GUI クラスの __init__ メソッドと ほぼ同様の処理 を記述します。

ただし、Mbtree_Anim クラスで表示する部分木の 選択されたノード は、後述の update_gui メソッドの中で、アニメーションの フレームの番号 を表す Play ウィジェットの value 属性から計算する ことになるので、Mbtree_GUI クラスの __init__ メソッドで行っていた、selectednode 属性にルートノードを代入する self.selectednode = self.mbtree.root という処理を 記述する必要ありません

__init__ メソッド以外のメソッドの定義は、この後で行います。

class Mbtree_Anim(GUI):
    def __init__(self, mbtree, size=0.15):
        self.mbtree = mbtree
        self.size = size
        self.width = 50
        self.height = 64
        super().__init__()
        
    def create_widgets(self):
        pass
    
    def display_widgets(self):
        pass
    
    def create_event_handler(self):
        pass
    
    def update_gui(self):
        pass
Mbtree_GUI クラスの __init__ メソッドからの修正箇所
    def __init__(self, mbtree, size=0.15):
        self.mbtree = mbtree
        self.size = size
        self.width = 50
        self.height = 64
-       self.selectednode = self.mbtree.root
        super().__init__()

create_widgets メソッドの定義

Mbtree_Anim クラスでは、Play ウィジェット、IntSlider ウィジェット、Figure を表示するので、create_widgets メソッドは下記のように定義します。その際に、アニメーションの fps を決める必要がありますが、今回の実装ではアニメーションがあまり速くなりすぎないように、2 fps に設定しました。

  • 2 行目:ゲーム木の ノードの数self.mbtree.nodenum に代入されているので、Play ウィジェットの value 属性の最大値 を表す max 属性には その値から 1 を引いた値を代入 する。list のインデックスが 0 から数え始めるので 1 を引く必要がある点に注意する事。また、fps が 2 のアニメーションを行うために、interval に 500 を代入した
  • 3 行目:IntSlider ウィジェットは Play ウィジェットとリンクさせるので、max 属性に同じ値を代入 する。また、Intslider の左に frame という文字列が表示 されるようにするために、description 属性に "frame" を代入 した
  • 4 行目:Play と IntSlider ウィジェットの value 属性をリンク させる
  • 6 ~ 13 行目:Figure を作成する。この部分は Mbtree_GUI と全く同じである
 1  def create_widgets(self):
 2      self.play = widgets.Play(max=self.mbtree.nodenum - 1, interval=500)
 3      self.slider = widgets.IntSlider(max=self.mbtree.nodenum - 1, description="frame")
 4      widgets.jslink((self.play, "value"), (self.slider, "value"))  
 5
 6      with plt.ioff():
 7          self.fig = plt.figure(figsize=[self.width * self.size,
 8                                          self.height * self.size])
 9          self.ax = self.fig.add_axes([0, 0, 1, 1])
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_Anim.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
    self.play = widgets.Play(max=self.mbtree.nodenum - 1, interval=500)
    self.slider = widgets.IntSlider(max=self.mbtree.nodenum - 1, description="frame")
    widgets.jslink((self.play, "value"), (self.slider, "value"))
    
    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_Anim.create_widgets = create_widgets

valueminstep 属性は、デフォルト値である 001 で良いので上記では記述しませんでしたが、それらの属性の値を明確にしたい人は下記のプログラムのように記述して下さい。

    self.play = widgets.Play(value=0, min=0, max=self.mbtree.nodenum - 1,
                             step=1, interval=500)
    self.slider = widgets.IntSlider(value=0, min=0, max=self.mbtree.nodenum - 1, 
                             step=1, description="frame")

display_widgets メソッドの定義

display_widgets メソッドは下記のプログラムのように定義します。

  • 2 行目:Play と IntSlider ウィジェットを横に配置した Hbox を作成する
  • 3 行目:上記の Hbox と Figure を縦に配置して表示する
def display_widgets(self):
    hbox = widgets.HBox([self.play, self.slider])
    display(widgets.VBox([hbox, self.fig.canvas]))
    
Mbtree_Anim.display_widgets = display_widgets

update_gui メソッドの定義

update_gui は下記のプログラムのように、Play ウィジェットの value 番目にゲーム木に挿入されたノード中心かつ選択されたノードとする部分木 を描画するように定義します。

  • 2、3 行目value 番目にゲーム木に挿入されたノード は、Mbtree クラスの nodelist 属性 に代入された list の value 番のインデックスの要素に代入されているので、self.mbtree.nodelist[self.play.value] で計算できる。それを selectednode 属性と centernode 属性に代入する
 1  def update_gui(self):
元と同じなので省略   
 2      self.selectednode = self.mbtree.nodelist[self.play.value]
 3      self.centernode = self.selectednode
 4      if self.selectednode.depth <= 4:
 5          maxdepth = self.selectednode.depth + 1
 6      elif self.selectednode.depth == 5:
 7          maxdepth = 7
 8      else:
 9          maxdepth = 9
10      self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
11
12  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.invert_yaxis()
    self.ax.axis("off")   
    
    self.selectednode = self.mbtree.nodelist[self.play.value]
    self.centernode = self.selectednode
    if self.selectednode.depth <= 4:
        maxdepth = self.selectednode.depth + 1
    elif self.selectednode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
    
Mbtree_Anim.update_gui = update_gui
修正箇所(Mbtree_GUI からの修正箇所です)
def update_gui(self):
元と同じなので省略   
+   self.selectednode = self.mbtree.nodelist[self.play.value]
+   self.centernode = self.selectednode
    if self.selectednode.depth <= 4:
        maxdepth = self.selectednode.depth + 1
    elif self.selectednode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode, ax=self.ax, maxdepth=maxdepth)
    
Mbtree_Anim.update_gui = update_gui

上記のプログラムでは、selectednode 属性と、centernode 属性に値を代入しましたが、それらの属性の値は update_gui メソッドの中でしか利用しない ので、ローカル変数に代入してもかまいません。ただし、そのようにすると Mbtree_GUI クラスの update_gui メソッドをコピーして Mbtree_Anim クラスの update_gui クラスを定義する際に、修正箇所が増えてしまうので、本記事では採用しませんでした。

create_event_handler メソッドの定義

create_event_handler には、下記のプログラムのように、Play ウィジェットの value 属性の値が変更された場合 に呼び出される イベントハンドラを定義 し、Play ウィジェットに結びつけます。また、このイベントハンドラが行う処理は、Figure の表示の更新だけ なので、update_gui を呼び出す処理のみを記述します。

def create_event_handler(self):
    def on_play_changed(changed):
        self.update_gui()
        
    self.play.observe(on_play_changed, names="value")
    
Mbtree_Anim.create_event_handler = create_event_handler

処理の確認

上記の修正後に、下記のプログラムを実行すると、実行結果の左図のように ルートノード中心かつ、選択された部分木 が表示されます。また、三角形のボタンをクリックすると、右図のようにゲーム木に 登録された順 で、赤い枠のノードが表示 される アニメーション が行われることと、IntSlider をドラッグ することで、アニメーションの 任意のフレームの表示に移動できる ことを確認して下さい。

mbtree_anim = Mbtree_Anim(mbtree)

実行結果(右図は 0 ~ 19 フレームまでのアニメーションの繰り返しです)

上記のプログラムでアニメーションを行う際に、たまに表示が固まる場合があり、そのような場合は、しばらくすると固まっていた間のフレームの表示が飛ばされて、その先のフレームが表示されます。

この現象はおそらく、以前の記事のノートで説明した、利用できなくなったオブジェクトのデータをメモリから削除する、ガーベジコレクションという処理を Python が行っていることが原因である可能性が高いのではないかと思います。

Play ウィジェットは、interval で指定した時間が経過した際に、ガーベジコレクションなどの理由でイベントハンドラが実行できなかった場合は、その時の value の値に対するイベントハンドラの 呼び出しの処理が飛ばされる ようになっているようです。そのため、ガーベジコレクションが発生した場合は、その間のフレームの表示が飛ばされて、その先のフレームが表示されます。

ガーベジコレクションは、いつ、どれくらいの長さで行われるかが予測できないので、アニメーションの処理の最中に行われると、アニメーションのフレームのいくつかが飛んでしまうことになります。

なお、Python のガーベジコレクションの仕様が良くわからなかったので、上記のガーベジコレクションに関する説明は間違っているかもしれません。間違っている場合は、コメントで指摘していただけると嬉しいです。

今回の記事のまとめ

今回の記事では、マウス操作による中心となるノードの移動に関する処理の修正と、ゲーム木の作成の過程を表示するアニメーションの実装を行いました。

ただし、今回作成したアニメーションにはいくつかの問題点があるので、ゲーム木の生成過程があまりわかりやすく表示されません。次回の記事ではそれらを修正することにします。余裕がある方は、どのような問題点があるかについて考えてみて下さい。

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

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

次回の記事

  1. 深さが 8 以下のノードで決着がついたため、深さが 9 のノードが存在しない場合は除きます

  2. Figure の上部に表示される Figure のタイトルのことです

  3. jsdlinkd は、おそらく方向を表す directional の d だと思います。

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?