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を一から作成する その119 matplotlib での文字の表示の性質と部分木の表示サイズを変更する FloatSlider の実装

Last updated at Posted at 2024-09-26

目次と前回の記事

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

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

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

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

Marubatsu_GUI クラスの問題点と改良

〇×ゲームの GUI の下に GUI の部分木を Marubatsu_GUI クラスを使って表示するようにしたことで、Marubatsu_GUI クラスにいくつかの問題点や改良したほうが良い点が生じるようになりました。どのような問題点や改良の余地があるかについて少し考えてみて下さい。

評価値の文字の表示に関する問題の修正

前回の記事の修正によって、下記のプログラムのように gui_play() を実行した際に、実行結果のように GUI の部分木の 局面の上に評価値を表示 するようにしましたが、下図からわかるように 評価値の文字の上端 が上にある ゲーム盤に重なって表示される という問題が発生しています。なお、gui_play() を実行して最初に表示される部分木の 0 という評価値は重なって表示されていることがわかりづらいので、下図では評価値として 1 が表示される局面を選択した場合の図にしました。

from util import gui_play

gui_play()

実行結果

ただし、下記のプログラムで Mbtree_GUI クラスのインスタンスを直接作成 してゲーム木の部分木を表示した場合は、実行結果のように評価値が上にある ゲーム盤に重なって表示されることはありません

from tree import Mbtree, Mbtree_GUI

mbtree = Mbtree.load("../data/aidata")
Mbtree_GUI(mbtree)

実行結果

評価値が重なって表示される問題の原因

上記の 2 つの違いが生じる理由は、matplotlib の Axes の text メソッドで文字を描画する際 に、表示される 文字の大きさ が Figure の大きさや、Axes の表示範囲に影響されず、常に同じ大きさで表示されるようになっている ことが原因です。

matplotlib での文字の表示の性質

具体例を挙げて説明します。

下記のプログラムは、matplotlib で以下のような図形と文字を 4 ~ 7、9 ~ 12、14 ~ 19 行目で 3 回描画するプログラムです。図形を描画する plot メソッドと 文字を描画する text メソッドの処理は 3 回の描画で全く同じ ですが、Figure の大きさAxes の表示範囲 は下記の表のように それぞれ異なります。なお、実際には 3 つの Figure が縦に並べて表示されますが、実行結果ではわかりやすさを重視して横に並べて表示しました。

  • plot メソッドで (0, 0)、(1, 0)、(0.5, 1) を頂点とする三角形を描画する
  • text メソッドで (0.5, 0) の座標に fontsize が 20 で A という文字を描画する
Figure の大きさ Axes の x 座標の表示範囲 Axes の y 座標の表示範囲
(1, 1) 0 ~ 1 0 ~ 1
(2, 2) 0 ~ 1 0 ~ 1
(2, 2) -1 ~ 2 -1 ~ 2
 1  import matplotlib.pyplot as plt
 2  import japanize_matplotlib
 3
 4  fig, ax = plt.subplots(figsize=(1, 1))
 5  ax.plot([0, 1, 0.5, 0], [0, 0, 1, 0])
 6  ax.text(0.5, 0, "A", fontsize=20)
 7  plt.show()
 8
 9  fig, ax = plt.subplots(figsize=(2, 2))
10  ax.plot([0, 1, 0.5, 0], [0, 0, 1, 0])
11  ax.text(0.5, 0, "A", fontsize=20)
12  plt.show()
13
14  fig, ax = plt.subplots(figsize=(2, 2))
15  ax.set_xlim(-1, 2)
16  ax.set_ylim(-1, 2)
17  ax.plot([0, 1, 0.5, 0], [0, 0, 1, 0])
18  ax.text(0.5, 0, "A", fontsize=20)
19  plt.show()
行番号のないプログラム
import matplotlib.pyplot as plt
import japanize_matplotlib

fig, ax = plt.subplots(figsize=(1, 1))
ax.plot([0, 1, 0.5, 0], [0, 0, 1, 0])
ax.text(0.5, 0, "A", fontsize=20)
plt.show()

fig, ax = plt.subplots(figsize=(2, 2))
ax.plot([0, 1, 0.5, 0], [0, 0, 1, 0])
ax.text(0.5, 0, "A", fontsize=20)
plt.show()

fig, ax = plt.subplots(figsize=(2, 2))
ax.set_xlim(-1, 2)
ax.set_ylim(-1, 2)
ax.plot([0, 1, 0.5, 0], [0, 0, 1, 0])
ax.text(0.5, 0, "A", fontsize=20)
plt.show()

実行結果

  

実行結果からわかるように 三角形の図形 は Figure の大きさや Axes の表示範囲が変化すると 見た目の大きさが変わります が、text メソッドで表示した A という文字は常に同じ大きさで表示 されます。

matplotlib では、Axes の text メソッドで表示される文字列 は、図形の描画とは異なり Figure の大きさや Axes の表示範囲に影響されず、キーワード引数 fontsize で設定された値が同じ であれば、常に同じ大きさで表示される

gui_play() で表示される GUI の部分木は、Marubatsu_GUI クラスの __init__ メソッドの中の下記のプログラムのように、部分木を表示する Figure の大きさを表すキーワード引数に size=0.1 を記述して Mbtree_GUI クラスのインスタンスを作成することで表示されます。

self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)

一方、Mbtree_GUI(mbtree) のように、Mbtree_GUI クラスのインスタンスを、キーワード引数 size を記述せずに作成 した場合は、下記の Mbtree_GUI クラスの __init__ メソッドの定義から、仮引数 size にはデフォルト値である 0.15 が代入 されます。

def __init__(self, mbtree, size=0.15):

そのため、gui_play() で表示される下図左の GUI の部分木を表示する Figure と、下図右の Mbtree_GUI(mbtree) で表示される部分木を表示する Figure の大きさの比率0.1 対 0.15 = 1 対 1.5 になるので、右図では左図の 1.5 倍の大きさで部分木が表示 されます。

一方で、評価値の文字 はどちらも 全く同じ大きさで表示 されますが、左図ではゲーム盤と ゲーム盤の表示の間隔右図よりも狭いため、評価値の文字の上部がゲーム盤に重なって表示されるようになります。

評価値の文字の大きさが同じ であることは、下図のようにそれぞれのゲーム盤と評価値の表示を横に並べ、評価値の文字の上と下に水平に線を引くことで確認することができます。

文字の大きさが変わらないことは、下記のプログラムのように キーワード引数に size=0.5 を記述して Mbtree_GUI クラスのインスタンスを作成することではっきりと確認することができます。実行結果のようにゲーム盤が大きく表示されますが、評価値の文字の大きさは明らかに変わっていません

Mbtree_GUI(mbtree, size=0.5)

実行結果

問題の修正方法

この問題は、Node クラスの draw_node評価値を表示する際 に、ゲーム盤の表示の大きさを表す 仮引数 size の値に 比例する大きさ で評価値の文字列を表示するようにすることで解決することができます。

size の値に比例する fontsize の値を計算する式は、size1 が代入されている場合の fontsize の値を計算し、その値に size を掛け算する という式で表すことができます。

これまでのプログラムでは、Mbtree_GUI(mbtree) を実行した際に仮引数 size にはデフォルト値である 0.15 が代入 され、評価値の文字draw_node 内の下記のプログラムでフォントサイズを表すキーワード引数 fontsize10 を指定して表示 しています。

ax.text(dx, y - 0.1, self.score, fontsize=10)

draw_node の修正

従って、size1 の場合の fontsize10 ÷ 0.15 = 66.666・・・ になり、fontsize の値は 66.666 * size という式で計算 することができます。ただし、66.666 という数字はきりが良くないので、下記のプログラムの 6 行目のように少し大きめのきりの良い 70 * size という式で fontsize を計算するように draw_node を修正することにします。

1  from tree import Node, Rect
2  from marubatsu import Marubatsu_GUI
3
4  def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):  
元と同じなので省略
5      if hasattr(self, "score"):
6          ax.text(dx, y - 0.1, self.score, fontsize=70*size)
元と同じなので省略
7
8  Node.draw_node = draw_node
行番号のないプログラム
from tree import Node, Rect
from marubatsu import Marubatsu_GUI

def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):  
    width = 8
    if ax is None:
        height = len(self.children) * 4
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")
        for childnode in self.children:
            childnode.height = 4
        self.height = height         
        
    # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
    y = dy + (self.height - 3) / 2
    bc = "red" if emphasize else None
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
                            score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
    if hasattr(self, "score"):
        ax.text(dx, y - 0.1, self.score, fontsize=70*size)
    rect = Rect(dx, y, 3, 3)
    # 子ノードが存在する場合に、エッジの線と子ノードを描画する
    if len(self.children) > 0:
        if maxdepth != self.depth:   
            ax.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="k", lw=lw)
            prevy = None
            for childnode in self.children:
                childnodey = dy + (childnode.height - 3) / 2
                if maxdepth is None:
                    Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True,
                                            score=getattr(childnode, "score", None), dx=dx+5, dy=childnodey, lw=lw)
                edgey = childnodey + 1.5
                ax.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="k", lw=lw)
                if prevy is not None:
                    ax.plot([dx + 4 , dx + 4], [prevy, edgey], c="k", lw=lw)
                prevy = edgey
                dy += childnode.height
        else:
            ax.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
            
    return rect

Node.draw_node = draw_node
修正箇所
from tree import Node, Rect
from marubatsu import Marubatsu_GUI

def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):  
元と同じなので省略
    if hasattr(self, "score"):
-       ax.text(dx, y - 0.1, self.score, fontsize=10)
+       ax.text(dx, y - 0.1, self.score, fontsize=70*size)
元と同じなので省略

Node.draw_node = draw_node

上記の修正後に、下記のプログラムを実行すると修正前と異なり、実行結果のように 評価値の文字が上のゲーム盤に重なって表示されてしまう という問題が発生することがわかります。その原因について少し考えてみて下さい。

Mbtree_GUI(mbtree)

実行結果

問題の検証

原因として、部分木の大きさを表す Mbtree_GUI クラスの __init__ メソッドの 仮引数 size の値評価値の文字の大きさに正しく反映されていない可能性がある ので、下記のプログラムのようにキーワード引数に size=0.5 を記述して実行してみると、実行結果のように 評価値の文字の大きさが変化しない ことから その可能性が高い ことがわかります。

Mbtree_GUI(mbtree, size=0.5)

実行結果

そこで、Mbtree_GUI クラスの __init__ メソッドの 仮引数 size の値が Node クラスの draw_node メソッドで 評価値を表示する際にどのように利用されるかを検証 します。

Mbtree_GUI クラスでは、update_gui メソッドによって ゲーム木の表示が更新される ので その処理を辿っていく と、下記の手順で Node クラスの draw_node が呼び出されて評価値の文字が表示されることが確認できます。

  • Mbtree_GUI クラスの __init__ メソッドで 仮引数 size の値が size 属性に代入 される
  • Mbtree_GUI クラスの update_gui メソッドで Mbtree クラスの draw_subtree メソッドが 呼び出される
  • Mbtree クラスの draw_subtree メソッドで Node クラスの draw_node メソッドが 呼び出される

まず update_gui を調べると、Mbtree クラスの draw_subtree メソッドを呼び出す 下記のプログラムには キーワード引数 size が記述されていない ことがわかります。

self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
                         show_bestmove=True, ax=self.ax, maxdepth=maxdepth)

従って、上記のプログラムが実行されると、下記の draw_subtree の定義の 2 行目から、draw_subtree の仮引数 size にはデフォルト値である 0.25 が代入 されることになります。

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

また、draw_subtree 内では Node クラスの draw_node メソッドが 5 箇所で呼ばれていますが、いずれも 下記のプログラムの 2 行目のように キーワード引数 size=size を記述 して呼び出しているので、draw_node の仮引数 size には 0.25 が代入 されることになります。

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

draw_node評価値を表示するプログラム は下記のように記述されているので、評価値の文字の大きさを表す fontsize には 70 * 0.25 = 17.5 という、今回の記事で 修正する前fontsize の値である 10 よりも大きな値が計算 されます。これが、Mbtree_GUI(mbtree) を実行した際に評価値の文字が修正前よりも大きく表示されてしまう原因です。

ax.text(dx, y - 0.1, self.score, fontsize=70*size)

問題の修正

この問題は、update_gui メソッドで draw_subtree を呼び出す際 に、キーワード引数 size を記述していないことが原因 なので、下記のプログラムの 3 行目のようにキーワード引数 size=self.size を記述することで修正することができます。

1  def update_gui(self):
元と同じなので省略
2      self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
3                       show_bestmove=True, ax=self.ax, maxdepth=maxdepth, size=self.size)
元と同じなので省略
4
5  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(-1, self.height - 1)   
    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,
                     show_bestmove=True, ax=self.ax, maxdepth=maxdepth, size=self.size)
    
    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):
元と同じなので省略
    self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
-                    show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
+                    show_bestmove=True, ax=self.ax, maxdepth=maxdepth, size=self.size)
元と同じなので省略

Mbtree_GUI.update_gui = update_gui

上記の修正後に下記のプログラムを実行することで、実行結果のように評価値の文字が正しい大きさで表示されるようになることが確認できます。

Mbtree_GUI(mbtree)

実行結果

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

Mbtree_GUI(mbtree, size=0.5)

実行結果

また、下記のプログラムのように gui_play() を実行した際でも評価値の大きさがゲーム盤の大きさに合わせた大きさで表示されるようになり、上のゲーム盤に重ならなくなったことが確認できます。

gui_play()

実行結果

以上で、評価値の文字の大きさの問題の修正は完了です。

FloatSlider ウィジェットによる部分木の表示の大きさの変更

Marubatsu_GUI クラスによって表示される部分木の表示の大きさは、インスタンスを作成した際にキーワード引数 size を記述することで設定できますが、後から部分木の表示の大きさを調整できるほうが便利 だと思いましたので、そのように改良することにします。

部分木の表示の大きさ は Marubatsu_GUI クラスの size 属性float 型のデータで代入される ので、その値を ipywidgets の float 型のデータを扱う FloatSlider というウィジェットを使って変更できるようにすることにします。FloatSlider は float 型のデータを扱えるという性質を除くと、以前の記事で紹介した整数を扱う IntSlider と同じ性質を持ちます。

下記は FloatSlider の主な属性です。

属性 意味 デフォルト値
value 現在の値 0
min 最小値 0
max 最大値 100
step 選択できる数値の間隔 0.1
description 左に表示される説明 ""
disabled True の場合に操作できなくなる False

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

create_widgets の修正

まず、下記のプログラムのように create_widgets の中で FloatSlider を作成します。下記のプログラムでは FloatSlider の最小値を 0.05、最大値を 0.25、数値の間隔を 0.01、初期値を self.size、説明文を size としましたが、別の値に設定したい方は自由に変更して下さい。

  • 5、6 行目:上記の設定の FloatSlider を作成し、size_slider 属性に代入する
1  import ipywidgets as widgets
2
3  def create_widgets(self):
元と同じなので省略
4      self.down_button = self.create_button("", 50)
5      self.size_slider = widgets.FloatSlider(min=0.05, max=0.25, step=0.01, 
6                                             description="size", value=self.size)
7      self.help_button = self.create_button("", 50)
元と同じなので省略
8
9  Mbtree_GUI.create_widgets = create_widgets 
行番号のないプログラム
import ipywidgets as widgets

def create_widgets(self):
    self.output = widgets.Output()  
    self.print_helpmessage()
    self.output.layout.display = "none"
    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.size_slider = widgets.FloatSlider(min=0.05, max=0.25, step=0.01, 
                                           description="size", value=self.size)
    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
    
Mbtree_GUI.create_widgets = create_widgets 
修正箇所
import ipywidgets as widgets

def create_widgets(self):
元と同じなので省略
    self.down_button = self.create_button("", 50)
+   self.size_slider = widgets.FloatSlider(min=0.05, max=0.25, step=0.01, 
+                                          description="size", value=self.size)
    self.help_button = self.create_button("", 50)
元と同じなので省略

Mbtree_GUI.create_widgets = create_widgets 

FloatSlider の値の範囲が 0.05 ~ 0.25 ではわかりづらいと思った方は、例えば 1 ~ 20 などの範囲の IntSlider に変更するという方法があります。その場合は、この後で修正する display_widgets で Figure のサイズを計算する式を、変更した値に応じた式に修正して下さい。

display_widgets の修正

次に、FloatSlider を どこに表示するかを決める 必要あります。本記事では下記のプログラムのように、2 行目の → ボタンと ? ボタンの間に配置することにしました。なお、→ ボタンと ? ボタンの間を空けるために配置した空白のウィジェットである self.label を間に入れると間が空きすぎるので入れないようにしました。

  • 4 行目:→ ボタンと ? ボタンのウィジェットの間に、FloatSlider のウィジェットを配置する。その際に間にあった self.label は削除した
1  def display_widgets(self):    
2      hbox1 = widgets.HBox([self.label, self.up_button, self.label])
3      hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
4                            self.size_slider, self.help_button])
5      hbox3 = widgets.HBox([self.label, self.down_button, self.label])
6      self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
7      display(self.vbox)  
8    
9  Mbtree_GUI.display_widgets = display_widgets
行番号のないプログラム
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.size_slider, self.help_button])
    hbox3 = widgets.HBox([self.label, self.down_button, self.label])
    self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
    display(self.vbox)  
    
Mbtree_GUI.display_widgets = display_widgets
修正箇所
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])
+                         self.size_slider, self.help_button])
    hbox3 = widgets.HBox([self.label, self.down_button, self.label])
    self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
    display(self.vbox)  
    
Mbtree_GUI.display_widgets = display_widgets

上記の修正後に下記のプログラムを実行すると、実行結果のように → ボタンと ? ボタンの間に FloatSlider が表示されるようになります。

Mbtree_GUI(mbtree)

実行結果

create_event_handler の修正

次に、FloatSlider の値を変更した際の イベントハンドラを定義する必要 があります。そのイベントハンドラでどのような処理を行えばよいかについて少し考えてみて下さい。

FloatSlider のイベントハンドラには下記の処理を記述する必要があります。

  • self.size の値を FloatSlider の更新された値に変更する
  • Figure のサイズを self.size の値に応じたサイズに変更する
  • 変更したサイズで 評価値の文字を表示し直す ために、update_gui を呼び出して 部分木の表示を更新する

Figure のサイズの変更は、Figure の set_figwidthset_figheight メソッドで行うことができます。詳しくは下記のリンク先を参照して下さい。

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

  • 2 ~ 6 行目:FloatSlider の値が変更された際に呼び出されるイベントハンドラを定義する
  • 3 行目size 属性に変更後の FloatSlider の値を表す changed["new"] を代入する
  • 4、5 行目:Figure のサイズを size 属性の値を使って計算した値に変更する
  • 6 行目:部分木の表示を更新する
  • 15 行目:イベントハンドラと FloatSlider を結び付ける
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_size_slider_changed(changed):
 3          self.size = changed["new"]
 4          self.fig.set_figwidth(self.width * self.size)
 5          self.fig.set_figheight(self.height * self.size)
 6          self.update_gui()
 7               
 8      def on_help_button_clicked(b=None):
 9          self.output.layout.display = "none" if self.output.layout.display is None else None
10                        
11      self.left_button.on_click(on_left_button_clicked)
12      self.right_button.on_click(on_right_button_clicked)
13      self.up_button.on_click(on_up_button_clicked)
14      self.down_button.on_click(on_down_button_clicked)
15      self.size_slider.observe(on_size_slider_changed, names="value")
16      self.help_button.on_click(on_help_button_clicked)
元と同じなので省略
17    
18  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_size_slider_changed(changed):
        self.size = changed["new"]
        self.fig.set_figwidth(self.width * self.size)
        self.fig.set_figheight(self.height * self.size)
        self.update_gui()
                
    def on_help_button_clicked(b=None):
        self.output.layout.display = "none" if self.output.layout.display is None else None
                        
    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.size_slider.observe(on_size_slider_changed, names="value")
    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_size_slider_changed(changed):
+       self.size = changed["new"]
+       self.fig.set_figwidth(self.width * self.size)
+       self.fig.set_figheight(self.height * self.size)
+       self.update_gui()
                
    def on_help_button_clicked(b=None):
        self.output.layout.display = "none" if self.output.layout.display is None else None
                        
    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.size_slider.observe(on_size_slider_changed, names="value")
    self.help_button.on_click(on_help_button_clicked)
元と同じなので省略
    
Mbtree_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、FloatSlider の値をドラッグして変更すると部分木の表示サイズが変更されることを確認して下さい。

Mbtree_GUI(mbtree)

評価値の表示の有無の切り替え

評価値の表示は、それぞれのノードの具体的な評価値を確認したい場合には便利ですが、〇の必勝の局面、引き分けの局面、×の必勝の局面を区別するだけであれば、ゲーム盤の色で区別できるので評価値を表示する必要はありません。必要のない情報を表示 すると、表示が複雑になってわかりづらくなる ので、必要に応じて評価値の表示の有無を切り替えることができる ように改良することにします。

Node クラスの draw_node の修正

評価値の表示の有無を切り替えることができるようにするためには、評価値を表示する処理を行う Node クラスの draw_node メソッドに、評価値を表示するかどうかを表す仮引数を追加する必要 があります。本記事では、その仮引数の名前を show_score とし、下記のプログラムのように draw_node を修正することにします。

  • 3 行目:デフォルト値を True とする仮引数 show_score を追加する
  • 4 行目:評価値を表示する条件に、show_scoreTrue であることを追加する
1  import matplotlib.patches as patches
2
3  def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, show_score=True, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
4      if hasattr(self, "score") and show_score:
5          ax.text(dx, y - 0.1, self.score, fontsize=70*size)
元と同じなので省略
6
7  Node.draw_node = draw_node
行番号のないプログラム
import matplotlib.patches as patches

def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, show_score=True, size=0.25, lw=0.8, dx=0, dy=0):
    width = 8
    if ax is None:
        height = len(self.children) * 4
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")
        for childnode in self.children:
            childnode.height = 4
        self.height = height         
        
    # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
    y = dy + (self.height - 3) / 2
    bc = "red" if emphasize else None
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, 
                            score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
    if hasattr(self, "score") and show_score:
        ax.text(dx, y - 0.1, self.score, fontsize=70*size)
    rect = Rect(dx, y, 3, 3)
    # 子ノードが存在する場合に、エッジの線と子ノードを描画する
    if len(self.children) > 0:
        if maxdepth != self.depth:   
            ax.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="k", lw=lw)
            prevy = None
            for childnode in self.children:
                childnodey = dy + (childnode.height - 3) / 2
                if maxdepth is None:
                    Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True,
                                            score=getattr(childnode, "score", None), dx=dx+5, dy=childnodey, lw=lw)
                edgey = childnodey + 1.5
                ax.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="k", lw=lw)
                if prevy is not None:
                    ax.plot([dx + 4 , dx + 4], [prevy, edgey], c="k", lw=lw)
                prevy = edgey
                dy += childnode.height
        else:
            ax.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
            
    return rect

Node.draw_node = draw_node
修正箇所
import matplotlib.patches as patches

-def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
+def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, show_score=True, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
-   if hasattr(self, "score"):
+   if hasattr(self, "score") and show_score:
        ax.text(dx, y - 0.1, self.score, fontsize=70*size)
元と同じなので省略

Node.draw_node = draw_node

Mbtree クラスの draw_subtree メソッドの修正

次に、draw_node を呼び出す Mbtree クラスの draw_subtree メソッドを下記のプログラムのように修正します。

  • 2 行目:デフォルト値を True とする仮引数 show_score を追加する
  • 4、6、8、10、12 行目:5 箇所ある draw_node の呼び出しすべてにキーワード引数 show_score を追加する
 1  def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None, isscore=False,
 2                   show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
 3                  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
 4                                        show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
 5                          rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize,
 6                                                    show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
 7                  rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness,
 8                                           show_score=show_score, lw=lw, dx=dx, dy=dy)
元と同じなので省略
 9          rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, 
10                                  show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
11              rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness,
12                                    show_score=show_score, size=size, lw=lw, dx=dx, dy=0)
13              self.nodes_by_rect[rect] = node
14            
15  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
            elif node.mb.last_move in node.parent.bestmoves:
                return 0
            else:
                return 0.2
            
        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
    
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(N=centernode, maxdepth=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")        
    
    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 = 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
                        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 = 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 = 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 = 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, size=0.25, lw=0.8, maxdepth=2):
+                show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness,
-                                     size=size, lw=lw, dx=dx, dy=dy)
+                                     show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
                        rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize,
-                                                 size=size, lw=lw, dx=dx, dy=dy)
+                                                 show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness,
-                                        size=size, lw=lw, dx=dx, dy=dy)
+                                        show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
        rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, 
-                               size=size, lw=lw, dx=dx, dy=dy)
+                               show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
            rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness,
-                                 size=size, lw=lw, dx=dx, dy=dy)
+                                 show_score=show_score, size=size, lw=lw, dx=dx, dy=dy)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree

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

次に、下記のプログラムのように、Mbtree_GUI クラスの インスタンスの作成時 に、キーワード引数 show_score によって 評価値の表示の有無を設定 できるように __init__ メソッドを修正します。

  • 1 行目:デフォルト値を True とする仮引数 show_score を追加する
  • 3 行目show_score を同名の属性に代入する
 1  def __init__(self, mbtree, show_score=True, size=0.15):
 2      self.mbtree = mbtree
 3      self.show_score = show_score
 4      self.size = size
 5      self.width = 50
 6      self.height = 65
 7      self.selectednode = self.mbtree.root
 8      super(Mbtree_GUI, self).__init__()
 9   
10  Mbtree_GUI.__init__ = __init__
行番号のないプログラム
def __init__(self, mbtree, show_score=True, size=0.15):
    self.mbtree = mbtree
    self.show_score = show_score
    self.size = size
    self.width = 50
    self.height = 65
    self.selectednode = self.mbtree.root
    super(Mbtree_GUI, self).__init__()
    
Mbtree_GUI.__init__ = __init__
修正箇所
-def __init__(self, mbtree, size=0.15):
+def __init__(self, mbtree, show_score=True, size=0.15):
    self.mbtree = mbtree
+   self.show_score = show_score
    self.size = size
    self.width = 50
    self.height = 65
    self.selectednode = self.mbtree.root
    super(Mbtree_GUI, self).__init__()
    
Mbtree_GUI.__init__ = __init__

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

最後に、下記のプログラムの 3 行目のように update_gui クラス内で draw_subtree を呼び出す際に、キーワード引数 show_score=self.show_score を記述するように修正します。

1  def update_gui(self):
元と同じなので省略
2      self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
3                               show_bestmove=True, show_score=self.show_score,
4                               ax=self.ax, maxdepth=maxdepth, size=self.size)
元と同じなので省略
5
6  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(-1, self.height - 1)   
    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,
                             show_bestmove=True, show_score=self.show_score,
                             ax=self.ax, maxdepth=maxdepth, size=self.size)
    
    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):
元と同じなので省略
    self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
-                            show_bestmove=True,
+                            show_bestmove=True, show_score=self.show_score,
                             ax=self.ax, maxdepth=maxdepth, size=self.size)
元と同じなので省略

Mbtree_GUI.update_gui = update_gui

実行結果は同じなので省略しますが、上記の修正後に下記のプログラムを実行すると実行結果からこれまでと同じようにゲーム木の部分木に評価値が表示されることが確認できます。

Mbtree_GUI(mbtree)

また、下記のプログラムのようにキーワード引数に show_score=False を記述すると、実行結果のように評価値が表示されなくなることが確認できます。

Mbtree_GUI(mbtree, show_score=False)

ボタンによる評価値の表示の有無の切り替え

上記のプログラムでは、Mbtree_GUI クラスのインスタンスを作成する際に評価値の表示の有無をキーワード引数で指定していますが、以前の記事でヘルプの表示の有無を ? ボタンで切り替えるように修正したように、評価値の表示の有無を切り替えるボタンを用意したほうが便利 でしょう。

create_widgets の修正

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

  • 3 行目:評価値の表示の有無を設定するボタンを作成する
1  def create_widgets(self):
元と同じなので省略
2      self.down_button = self.create_button("", 50)
3      self.score_button = self.create_button("評価値の表示", 100)
元と同じなので省略
4    
5  Mbtree_GUI.create_widgets = create_widgets 
行番号のないプログラム
def create_widgets(self):
    self.output = widgets.Output()  
    self.print_helpmessage()
    self.output.layout.display = "none"
    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.score_button = self.score_button = self.create_button("評価値の表示", 100)
    self.size_slider = widgets.FloatSlider(min=0.05, max=0.25, step=0.01, description="size", value=self.size)
    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
    
Mbtree_GUI.create_widgets = create_widgets 
修正箇所
def create_widgets(self):
元と同じなので省略
    self.down_button = self.create_button("", 50)
+   self.score_button = self.score_button = self.create_button("評価値の表示", 100)
元と同じなので省略
    
Mbtree_GUI.create_widgets = create_widgets 

display_widgets の修正

次に、このボタンを どこに表示するかを決める必要 あります。本記事では下記のプログラムのように、1 行目の ↑ ボタンの右、2 行目の FloatSlider の上に配置することにしました。なお、FloatSlider の上に配置するために、↑ ボタンの右に self.label を 2 つ配置して表示位置を調整しました。

  • 2 行目:↑ ボタンの右に、self.label を 2 つはさんでボタンを配置する
1  def display_widgets(self):    
2      hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
3      hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
4                            self.size_slider, self.help_button])
5      hbox3 = widgets.HBox([self.label, self.down_button, self.label])
6      self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
7      display(self.vbox)  
    
Mbtree_GUI.display_widgets = display_widgets
行番号のないプログラム
def display_widgets(self):    
    hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
    hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
                          self.size_slider, self.help_button])
    hbox3 = widgets.HBox([self.label, self.down_button, self.label])
    self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
    display(self.vbox)  
    
Mbtree_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):    
-   hbox1 = widgets.HBox([self.label, self.up_button])
+   hbox1 = widgets.HBox([self.label, self.up_button, self.label, self.label, self.score_button])
    hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
                          self.size_slider, self.help_button])
    hbox3 = widgets.HBox([self.label, self.down_button, self.label])
    self.vbox = widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas])
    display(self.vbox)  
    
Mbtree_GUI.display_widgets = display_widgets

上記の修正後に、下記のプログラムを実行すると、実行結果のように ↑ ボタンの右に評価値の表示の有無を切り替えるボタンが表示されるようになります。

Mbtree_GUI(mbtree)

実行結果

create_event_handler の修正

次に、下記のプログラムのように、このボタンをクリックした際に実行する イベントハンドラを create_event_handler の中で定義 します。

  • 2 ~ 4 行目:評価値の表示の有無を変更するボタンがクリックされた際のイベントハンドラを定義する。行う処理は、not 演算子で show_score 属性の値の TrueFalse を反転し、update_gui メソッドを呼び出して表示を更新する処理である
  • 19 行目:ボタンとイベントハンドラを結び付ける
 1  def create_event_handler(self):
元と同じなので省略              
 2      def on_score_button_clicked(b=None):
 3          self.show_score = not self.show_score
 4          self.update_gui()
 5                  
 6      def on_size_slider_changed(changed):
 7          self.size = changed["new"]
 8          self.fig.set_figwidth(self.width * self.size)
 9          self.fig.set_figheight(self.height * self.size)
10          self.update_gui()
11                
12      def on_help_button_clicked(b=None):
13          self.output.layout.display = "none" if self.output.layout.display is None else None
14                        
15      self.left_button.on_click(on_left_button_clicked)
16      self.right_button.on_click(on_right_button_clicked)
17      self.up_button.on_click(on_up_button_clicked)
18      self.down_button.on_click(on_down_button_clicked)
19      self.score_button.on_click(on_score_button_clicked)
元と同じなので省略
20    
21  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_score_button_clicked(b=None):
        self.show_score = not self.show_score
        self.update_gui()
                
    def on_size_slider_changed(changed):
        self.size = changed["new"]
        self.fig.set_figwidth(self.width * self.size)
        self.fig.set_figheight(self.height * self.size)
        self.update_gui()
                
    def on_help_button_clicked(b=None):
        self.output.layout.display = "none" if self.output.layout.display is None else None
                        
    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.score_button.on_click(on_score_button_clicked)
    self.size_slider.observe(on_size_slider_changed, names="value")
    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_score_button_clicked(b=None):
+       self.show_score = not self.show_score
+       self.update_gui()
                
    def on_size_slider_changed(changed):
        self.size = changed["new"]
        self.fig.set_figwidth(self.width * self.size)
        self.fig.set_figheight(self.height * self.size)
        self.update_gui()
                
    def on_help_button_clicked(b=None):
        self.output.layout.display = "none" if self.output.layout.display is None else None
                        
    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.score_button.on_click(on_score_button_clicked)
元と同じなので省略
    
Mbtree_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、下記のプログラムを実行し、「評価値の表示」ボタンをクリックすることで、評価値の表示の有無が切り替わることを確認して下さい。

Mbtree_GUI(mbtree)

評価値の表示の有無によるボタンの表示の色の変更

現状では、「評価値の表示」ボタンをクリックしても ボタンの色が変わらない点がわかりづらい という問題があります。そこで、評価値を表示するかどうか で「評価値の表示」ボタンの色を下記の表のように変更する ように改良します。下記のようにボタンの表示の色を変更することで、評価値の表示の有無のような、ON/OFF の状態を色で区別できるボタン を作成することができるようになります。

なお、下記の表とは別の色を設定したい人は自由に変更して下さい。

状態 ボタンの色
評価値が表示されている 薄い緑(lightgreen)
評価値が表示されていない 濃い灰色(darkgray)

GUI クラスの set_button_color メソッドの定義

ボタンの表示の色の変更を行うメソッドとして、以前の記事で GUI クラスの set_button_status メソッドで定義しましたが、このメソッドは ボタンが操作できない状態(disabled)であるか どうかで ボタンの色を変更 する処理を行うため、ボタンを操作できる状態で色を変更する という、上記の用途で 利用することはできません。そこで、下記のプログラムのように、ボタンの状態 を表す仮引数 value の値によってボタンの色を変更する set_button_color メソッドを GUI クラスに定義する事にします。

  • 3 行目set_button_status と同様に静的メソッドとして定義する
  • 4、5 行目:仮引数 value に代入された値によって、仮引数 button に代入されたボタンの表示の色を上記の表のように変更する
1  from gui import GUI
2
3  @staticmethod
4  def set_button_color(button, value):      
5      button.style.button_color = "lightgreen" if value else "darkgray"
6        
7  GUI.set_button_color = set_button_color   
行番号のないプログラム
from gui import GUI

@staticmethod
def set_button_color(button, value):      
    button.style.button_color = "lightgreen" if value else "darkgray"
        
GUI.set_button_color = set_button_color   

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

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

3 行目set_button_color メソッドを呼び出すことで、show_score 属性の値によって、「評価値の表示」ボタンの色を変更するようにする

1  def update_gui(self):
元と同じなので省略
2      self.set_button_status(self.down_button, disabled=disabled)
3      self.set_button_color(self.score_button, value=self.show_score)
4
5  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(-1, self.height - 1)   
    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,
                             show_bestmove=True, show_score=self.show_score,
                             ax=self.ax, maxdepth=maxdepth, size=self.size)
    
    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)
    self.set_button_color(self.score_button, value=self.show_score)

Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
    self.set_button_status(self.down_button, disabled=disabled)
+   self.set_button_color(self.score_button, value=self.show_score)

Mbtree_GUI.update_gui = update_gui

上記の修正後に下記のプログラムを実行し、「評価値の変更」ボタンをクリックすると、実行結果のようにボタンの色が灰色になり評価値が表示されなくなることを確認して下さい。

Mbtree_GUI(mbtree)

実行結果

また、実行結果は省略しますが、下記のプログラムで gui_play() を実行した場合に GUI の部分木の FloatSlider や「評価値の表示」ボタンによる操作が行えることを確認して下さい。

gui_play()

ボタンの表示に関する補足

? ボタンと「評価値の表示」ボタンに関するいくつかの補足説明を行います。

? ボタンに関する補足

? ボタンもヘルプの表示の有無を変更するボタンなので、「評価値の表示」ボタンと同様に、ON/OFF で色を変更するようにしてみたのですが、ヘルプを表示していない場合に ? ボタンを暗い灰色で表示 すると ? ボタンが目立たなくなるのがあまりよくない 気がしましたので ? ボタンは 常に緑色で表示することにしました

? ボタンも「評価値の表示」ボタンと同様に色を変更する場合は、update_gui の最後に下記のプログラムを追加して下さい。

  • 1 行目:ヘルプが表示されているかどうかを表す output.layout.display の値によって、? ボタンの ON/OFF を表す値を計算する
1  value = self.output.layout.display is None
2  self.set_button_color(self.help_button, value=value)

また、上記の変更だけでは、? ボタンをクリックした際にボタンの色の表示が変更されないので、下記のプログラム 4 行目のように create_event_handler の中の on_help_button_clicked の最後に表示の更新を行う処理を追加して下さい。

1  def create_event_handler(self):
元と同じなので省略
2      def on_help_button_clicked(b=None):
3          self.output.layout.display = "none" if self.output.layout.display is None else None
4          self.update_gui()
元と同じなので省略 
5    
6  Mbtree_GUI.create_event_handler = create_event_handler

ToggleButton に関する補足

ipywidgets にある程度詳しい人は、ON/OFF の区別 をつけることができる ToggleButton というウィジェットを使えばもっと簡単にプログラムを記述できるのではないかと思った人がいるかもしれません。筆者も最初は ToggleButton を使って「評価値の表示」ボタンを作成してみたのですが、ToggleButton で表示される ボタンの色 は、特定の配色でしか表示できず、その 配色があまりしっくりこなかった ので、本記事では ToggleButton を利用しないことにしました。参考までに ToggleButton の簡単な使い方についてこの後で紹介しますので、興味がある方は「評価値の表示」ボタンを ToggleButton で作成してみて下さい。

また、他にも CheckBox というウィジェットを利用するという方法もありますが、CheckBox は 表示の幅がボタンよりも広くなってしまう という問題があるので本記事では採用しませんでした。CheckBox を使ってみたい方は、下記のリンク先を見て下さい。

ToggleButton の性質

ToggleButton は以下のような性質を持つボタンです。

  • TrueFalse のいずれかの値を持つ
  • クリックすることで TrueFalse が入れ替わる
  • 設定されている値によって 自動的に表示の色が変わる ので、ボタンの色を変化させる処理を記述しなくても、どちらの値が設定されているかが見た目からわかる

下記は ToggleButton の主な属性です。

属性 意味 デフォルト値
value 現在の値(True または False False
description ToggleButton に表示される文字列 ""
disabled True の場合に操作できなくなる False

ToggleButton の作成と表示

下記は ToggleButton を作成して表示するプログラムです。実行結果の左図は ToggleButton に True が設定されている場合、右図は False が設定されている場合で、ToggleButton の色からどちらが設定されているかが一目でわかるようになっています。表示されたボタンを実際にクリックして確かめてみてください。

tb = widgets.ToggleButton(description="評価値の表示")
display(tb)

実行結果

 

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

ToggleButton の色の変更

ToggleButton は、初期設定 では 濃い灰色が True薄い灰色が False を表します。本記事でこれまで作成した GUI では、操作できない状態のボタンを薄い灰色 で、操作できるボタンを薄い緑色で表示 していたので、初期設定の ToggleButton の配色 では、操作できない状態のボタンFalse が設定された ToggleButton区別がつけづらい という問題が発生します。また、ToggleButton は通常のボタンと異なり style.button_color 属性によって 表示の色を変更することができません。これが ToggleButton を採用しなかった理由です。

なお、下記のプログラムのように、ToggleButton の button_style 属性を変更することでボタンの色を 特定のスタイルの色に変更 することができます。

tb = widgets.ToggleButton(description="評価値の表示", button_style="success")
display(tb)

実行結果

残念ながら button_style 属性に設定できるスタイルの種類は、下記のリンク先が示すように 5 種類しかないようです。いずれのスタイルの配色も 〇×ゲームの GUI の配色としては筆者はピンとこなかったので採用しないことにしました。興味がある方はそれぞれの配色で ToggleButton を実際に表示してみて下さい。

今回の記事のまとめ

今回の記事では以下の説明を行いました。

  • matplotlib での文字の表示の性質
  • FloatSlider を用いた、部分木の表示の変更機能の実装
  • 評価値の表示の有無を変更するボタンの実装

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

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

次回の記事

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?