目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
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
の値を計算する式は、size
に 1
が代入されている場合の fontsize
の値を計算し、その値に size
を掛け算する という式で表すことができます。
これまでのプログラムでは、Mbtree_GUI(mbtree)
を実行した際に仮引数 size
にはデフォルト値である 0.15
が代入 され、評価値の文字 は draw_node
内の下記のプログラムでフォントサイズを表すキーワード引数 fontsize
に 10
を指定して表示 しています。
ax.text(dx, y - 0.1, self.score, fontsize=10)
draw_node
の修正
従って、size
が 1
の場合の fontsize
は 10 ÷ 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_figwidth
と set_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_score
がTrue
であることを追加する
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
属性の値のTrue
とFalse
を反転し、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 は以下のような性質を持つボタンです。
-
True
とFalse
のいずれかの値を持つ - クリックすることで
True
とFalse
が入れ替わる - 設定されている値によって 自動的に表示の色が変わる ので、ボタンの色を変化させる処理を記述しなくても、どちらの値が設定されているかが見た目からわかる
下記は 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 |
次回の記事