目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
循環参照の問題の修正
前回の記事で修正したプログラムを marubatsu.py と tree.py に反映させた後で、下記のプログラムを実行すると、実行結果のように 循環インポート(circular import)の エラーが発生 します。また、エラーメッセージから、from util import gui_play
を実行した際にこのエラーが発生したことが確認できます。
なお、今回の記事の github の marubatsu.py は、このエラーが発生しないように 原因となる from tree import Mbtree, Mbtree_GUI
をコメントにしています。このエラーを確認したい人はそのコメントを外してから下記のプログラムを実行して下さい。
from util import gui_play
gui_play()
実行結果
---------------------------------------------------------------------------
ImportError Traceback (most recent call last)
Cell In[1], line 1
----> 1 from util import gui_play
3 gui_play()
略
File c:\Users\ys\ai\marubatsu\117\tree.py:4
2 import matplotlib.pyplot as plt
3 import matplotlib.patches as patches
----> 4 from marubatsu import Marubatsu, Marubatsu_GUI
5 from gui import GUI
6 import ipywidgets as widgets
ImportError: cannot import name 'Marubatsu' from partially initialized module 'marubatsu' (most likely due to a circular import) (c:\Users\ys\ai\marubatsu\117\marubatsu.py)
循環参照が起きる原因は以下の通りです。
-
前回の記事で Marubatsu_GUI クラスの
__init__
メソッド内で、mbtree.py で定義された Mbtree と Mbtree_GUI クラスを利用することになったので、marubatsu.py の先頭でfrom tree import Mbtree, Mbtree_GUI
を実行 してそれらのクラスを mbtree モジュールからインポートするように修正した -
util.py では、先頭の行の
from marubatsu import Marubatsu
で marubatsu モジュールから Marubatsu クラスがインポートされているのでmarubatsu.py
が実行 される -
marubatsu.py で
from tree import Mbtree, Mbtree_GUI
が実行された結果、tree.py が実行される -
tree.py には
from marubatsu import Marubatsu, Marubatsu_GUI
が記述されているので、marubatsu.py が再び実行 され、循環インポートが発生する
marubatu.py と mbtree.py を一つのファイルに統合することで循環インポートが発生しないようにすることができますが、それでは 〇×ゲーム と、ゲーム木の処理 を別のファイルで 分けて記述することができなくなってしまいます。
ファイルを統合せずに この問題を 解決するの方法の一つ に、下記のプログラムのように Marubatsu_GUI クラスの __init__
メソッドの中 で Mbtree と Mbtree_GUI クラスをインポートする という方法があります。
-
6 行目:
__init__
メソッドの中で Mbtree と Mbtree_GUI クラスをインポートする
1 from marubatsu import Marubatsu_GUI
2 from tkinter import Tk, filedialog
3 import os
4
5 def __init__(self, mb, params, names, ai_dict, seed, size):
元と同じなので省略
6 from tree import Mbtree, Mbtree_GUI
7 if Marubatsu_GUI.mbtree is None:
8 Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
9 self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
10
11 Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
from marubatsu import Marubatsu_GUI
from tkinter import Tk, filedialog
import os
def __init__(self, mb, params, names, ai_dict, seed, size):
if params is None:
params = [{}, {}]
if ai_dict is None:
ai_dict = {}
if names is None:
names = [None, None]
for i in range(2):
if names[i] is None:
if mb.ai[i] is None:
names[i] = "人間"
else:
names[i] = mb.ai[i].__name__
# JupyterLab からファイルダイアログを開く際に必要な前処理
root = Tk()
root.withdraw()
root.call('wm', 'attributes', '.', '-topmost', True)
# save フォルダが存在しない場合は作成する
if not os.path.exists("save"):
os.mkdir("save")
self.mb = mb
self.ai_dict = ai_dict
self.params = params
self.names = names
self.seed = seed
self.size = size
super(Marubatsu_GUI, self).__init__()
from tree import Mbtree, Mbtree_GUI
if Marubatsu_GUI.mbtree is None:
Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
Marubatsu_GUI.__init__ = __init__
修正箇所
from marubatsu import Marubatsu_GUI
from tkinter import Tk, filedialog
import os
def __init__(self, mb, params, names, ai_dict, seed, size):
元と同じなので省略
from tree import Mbtree, Mbtree_GUI
if Marubatsu_GUI.mbtree is None:
Marubatsu_GUI.mbtree = Mbtree.load("../data/aidata")
self.mbtree_gui = Mbtree_GUI(Marubatsu_GUI.mbtree, size=0.1)
Marubatsu_GUI.__init__ = __init__
この方法で循環インポートが起きなくなる理由は以下の通りです。
- 関数やメソッドの中に記述したモジュールのインポート は、その関数やメソッドが 呼び出されるまでは行われない
- Marubatsu_GUI クラスの
__init__
メソッドが呼び出される のは、from util import gui_play
を実行した後 なので、from util import gui_play
を実行した際に 循環インポートは発生しない - Marubatsu_GUI クラスの
__init__
メソッドが実行され、その中でfrom tree import Mbtree, Mbtree_GUI
を実行した際 には、既にそれらのモジュールインポートされている ので、循環インポートにはならない
このような 関数やメソッドのブロックの中に記述されたインポート のことを、ローカルなインポート と呼びます。ローカルなインポートは ローカル名前空間によって管理される ので、ローカル変数と同様に、その関数やメソッドの ブロックの中でしか利用できない という性質があります。
実行結果は省略しますが、上記の修正を行った後で下記のプログラムを実行すると、エラーが発生しなくなったことが確認できます。なお、marubatsu.py の先頭の from tree import Mbtree, Mbtree_GUI
のコメントを外した方は、元に戻した後で JupyterLab を再起動してからから下記のプログラムを実行して下さい。
gui_play()
最善手の表示
今回の記事では、前回の記事で開始した Mbtree_GUI クラスの改良の続きを行います。
現状の GUI の部分木1の表示 では、色によって局面の評価値がわかるようになっていますが、〇 の手番と × の手番によって評価値が大きいほうが最善手であるか、小さいほうが最善手であるかが変わるので、どの合法手が最善手であるかが直観的にわかりにくい という問題があります。そこで、最善手が着手されていない局面を暗い色で表示する ようにすることで、どの合法手が最善手であるか を一目でわかるようにすることにします。
まず、それぞれの局面が、最善手を着手した局面であるかを判定する 必要があります。どのように判定すればよいかについて少し考えてみて下さい。
最善手を着手した局面であるかの判定方法
aidata.mbtree に保存されたゲーム木のデータには、それぞれのノードの bestmoves
属性に最善手の一覧のデータが記録 されています。また、直前に行われた着手 は、ノードの mb
属性の lastmove
属性に記録 されています。そのため、下記の手順でそれぞれのノードの局面が最善手を着手した局面であるかを判定することができます。なお、着手が行われていない、ゲーム開始時の局面 であるルートノードは 例外として最善手を着手した局面であると判定する ことにします。
- 親ノードが存在しない場合 はルートノードなので 最善手を着手した と判定する
-
lastmove
属性が、親ノードのbestmoves
属性の要素に 含まれている場合 は 最善手を着手した と判定する - 上記のいずれでもない 場合は 最善手を着手していない と判定する
ゲーム木の ノードの描画 は Node クラスの draw_node
メソッド内で行われるので、下記の処理を記述することで最善手を着手していない局面を暗く表示することができます。
- ノードを描画 する処理を 行った後 で、最善手を着手した局面であるかを判定 する
- 最善手を着手した局面でないと判定された場合は、以前の記事で説明した方法で、その上に 半透明な黒い正方形の画像を描画する ことでその局面を 暗く表示する
draw_board
の修正
以前の記事でノードを暗く表示する処理を実装する際に、「draw_board
を修正するということを続けていくと、draw_board の定義が複雑になるという問題が発生する」という理由でノードを暗く表示する処理を、draw_node
とは別に実装しました。
その時はノードを暗くする処理を他の場所で行うことはないだろうと思っていたのですが、今回再び ノードを暗く表示する処理を新たに記述する ことになり、そのような 同じ処理を何度も記述するのは良くないと思い直しました ので、今回の記事では draw_node
内 でゲーム盤を暗く表示する 処理を記述する ように修正することにします。
今回の記事では行いませんが、今後ゲーム盤の 外枠 を 別の色や太さ で 描画したくなることがあるかもしれない ことを考慮して、draw_node
に 下記の仮引数を追加する ことにします。また、これまでは仮引数 emphasize
によって外枠を赤く表示していましたが、外枠の描画を下記の仮引数で指定できるようになるので仮引数 emphasize
は削除 します。
仮引数 | 意味 | デフォルト値 |
---|---|---|
bc 2
|
ゲーム盤の外枠の色。None の場合は外枠を描画しない |
None |
bw 3
|
ゲーム盤の外枠の幅 | 1 |
darkness |
ゲーム盤の表示の暗さ(darkness)を表す 0 ~ 1 の数値数値が大きい程暗くなる |
0 |
下記はそのように draw_board
を修正したプログラムです。
-
5 行目:上記の仮引数を追加し、仮引数
emphasize
を削除する - 途中にあった
emphasize
がTrue
の場合に外枠を描画する処理を削除する -
7 ~ 9 行目:ゲーム盤の描画を行った後で、
darkness
が0
より大きい場合に、ゲーム盤の上に半透明の黒い正方形を描画して暗くする -
12 ~ 15 行目:
bc
がNone
出ない場合は、ゲーム盤の外枠をbc
の色で、bw
の太さで描画する。この処理は、ゲーム盤を暗く表示する処理を行った後で行わないと枠の色まで暗くなってしまう点に注意すること
1 from marubatsu import Marubatsu
2 import matplotlib.patches as patches
3
4 @staticmethod
5 def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2):
元と同じなので省略
6 # darkness 0 より大きい場合は、半透明の黒い正方形を描画して暗くする
7 if darkness > 0:
8 ax.add_artist(patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
9 height=mb.BOARD_SIZE, fc="black", alpha=darkness))
10
11 # bc が None でない場合はその色で bw の太さで外枠を描画する
12 if bc is not None:
13 frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
14 height=mb.BOARD_SIZE, ec=bc, fill=False, lw=bw)
15 ax.add_patch(frame)
16
17 Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
from marubatsu import Marubatsu
import matplotlib.patches as patches
@staticmethod
def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=0, dx=0, dy=0, lw=2):
# 結果によってゲーム盤の背景色を変更する
if show_result:
if score is None and mb.status == Marubatsu.PLAYING:
bgcolor = "white"
elif score == 1 or mb.status == Marubatsu.CIRCLE:
bgcolor = "lightcyan"
elif score == -1 or mb.status == Marubatsu.CROSS:
bgcolor = "lavenderblush"
else:
bgcolor = "lightyellow"
rect = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
height=mb.BOARD_SIZE, fc=bgcolor)
ax.add_patch(rect)
# ゲーム盤の枠を描画する
for i in range(1, mb.BOARD_SIZE):
ax.plot([dx, dx + mb.BOARD_SIZE], [dy + i, dy + i], c="k", lw=lw) # 横方向の枠線
ax.plot([dx + i, dx + i], [dy, dy + mb.BOARD_SIZE], c="k", lw=lw) # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(mb.BOARD_SIZE):
for x in range(mb.BOARD_SIZE):
color = "red" if (x, y) == mb.last_move else "black"
Marubatsu_GUI.draw_mark(ax, dx + x, dy + y, mb.board[x][y], color, lw=lw)
# darkness 0 より大きい場合は、半透明の黒い正方形を描画して暗くする
if darkness > 0:
ax.add_artist(patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
height=mb.BOARD_SIZE, fc="black", alpha=darkness))
# bc が None でない場合はその色で bw の太さで外枠を描画する
if bc is not None:
frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
height=mb.BOARD_SIZE, ec=bc, fill=False, lw=bw)
ax.add_patch(frame)
Marubatsu_GUI.draw_board = draw_board
修正箇所
from marubatsu import Marubatsu
import matplotlib.patches as patches
@staticmethod
-def draw_board(ax, mb, show_result=False, score=None, dx=0, dy=0, lw=2):
+def draw_board(ax, mb, show_result=False, score=None, bc=None, bw=1, darkness=None, dx=0, dy=0, lw=2):
元と同じなので省略
# emphasize が True の場合は赤色の外枠を描画する
- if emphasize:
- frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
- height=mb.BOARD_SIZE, ec="red", fill=False, lw=lw)
- ax.add_patch(frame)
元と同じなので省略
# darkness 0 より大きい場合は、半透明の黒い正方形を描画して暗くする
+ if darkness > 0:
+ ax.add_artist(patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
+ height=mb.BOARD_SIZE, fc="black", alpha=darkness))
# bc が None でない場合はその色で bw の太さで外枠を描画する
+ if bc is not None:
+ frame = patches.Rectangle(xy=(dx, dy), width=mb.BOARD_SIZE,
+ height=mb.BOARD_SIZE, ec=bc, fill=False, lw=bw)
+ ax.add_patch(frame)
Marubatsu_GUI.draw_board = draw_board
上記の修正後に、下記のプログラムで draw_board
を新しい仮引数に対応する実引数を記述せずに呼び出すと、実行結果のようにこれまで通りの描画が行われることが確認できます。
import matplotlib.pyplot as plt
mb = Marubatsu()
fig, ax = plt.subplots(figsize=(1,1))
ax.axis("off")
Marubatsu_GUI.draw_board(ax, mb)
実行結果
また、下記のプログラムで外枠の色を赤に、暗さを 0.3 に設定して draw_board
を呼び出すと、実行結果のように指定した設定で描画が行われることが確認できます。
fig, ax = plt.subplots(figsize=(1,1))
ax.axis("off")
Marubatsu_GUI.draw_board(ax, mb, bc="red", darkness=0.3)
draw_node
の修正
mbtree.py の中で、draw_board
を呼び出している のは、Node クラスの draw_node
メソッドなので、draw_node
を下記のプログラムのように正する必要があります。
-
3 行目:デフォルト値を
0
とする仮引数darkness
を追加する。なお、ノードを強調して表示すかどうかを表す仮引数emphasize
はそのままで良い -
6 行目:
emphasis
の値によって、draw_board
を呼び出す際のキーワード引数bc
の値を計算する -
7、8 行目:
draw_board
を呼び出す際に、キーワード引数bc
とdarkness
を追加する
なお、draw_node
内では maxdepth
が None
の場合 に子ノードを draw_board
を呼び出して描画 する処理を行っていますが、そちらはゲーム木の部分木を描画する draw_subtree
から draw_board
を呼び出す際には実行されない ので、修正していません。
1 from tree import Node, Rect
2
3 def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
4 # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
5 y = dy + (self.height - 3) / 2
6 bc = "red" if emphasize else None
7 Marubatsu_GUI.draw_board(ax, self.mb, show_result=True,
8 score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
元と同じなので省略
9
10 Node.draw_node = draw_node
行番号のないプログラム
from tree import Node, Rect
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)
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
-def draw_node(self, ax=None, maxdepth=None, emphasize=False, size=0.25, lw=0.8, dx=0, dy=0):
+def draw_node(self, ax=None, maxdepth=None, emphasize=False, darkness=0, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
# 自分自身のノードを真ん中の位置になるように (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), lw=lw,
+ score=getattr(self, "score", None), bc=bc, darkness=darkness, lw=lw, dx=dx, dy=y)
元と同じなので省略
Node.draw_node = draw_node
上記の修正後に、下記のプログラムで draw_node
を新しい仮引数に対応する実引数を記述せずに 呼び出すと、実行結果のようにこれまで通りの描画が行われることが確認できます。
from tree import Mbtree
mbtree = Mbtree.load("../data/aidata")
mbtree.root.draw_node()
実行結果
また、下記のプログラムで draw_node
を実引数に emphasize=True
と darkness=0.3
を記述して呼び出すと、実行結果のように指定した設定で描画が行われることが確認できます。なお、上記で説明したように実引数 maxdepth
を記述せずに draw_node
を呼び出した際に表示される子ノードには、emphasize
と drakness
の設定は影響を与えません。
mbtree.root.draw_node(emphasize=True, darkness=0.3)
また、実行結果はこれまでと同じなので省略しますが、下記のプログラムで Marubatsu_GUI
のインスタンスを作成すると、これまでと同様に GUI でゲーム木の部分木が表示されることが確認できます。実際に確認してみて下さい。
from tree import Mbtree_GUI
Mbtree_GUI(mbtree)
Mbtree_Anim クラスに対応した修正の必要性
Mbtree_GUI クラスによるゲーム木の部分木の表示では、ノードを暗く表示する処理は行っていません が、Mbtree_Anim クラス でゲーム木の生成過程や、評価値の計算過程を表示する際には、まだ作成されていないノードや評価値が計算されていないノードを 暗く表示する処理 が下記の update_gui
メソッド内の 5 ~ 8 行目に記述されています。
-
5 行目:ノードが作成された順番または、評価値が計算された順番を計算して
index
に代入する -
6 ~ 8 行目:
index
が 表示中のアニメーションのフレーム数 を表すself.play.value
より大きい場合 にそのノードを 暗く表示する
1 def update_gui(self):
略
2 self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
3 ax=self.ax, maxdepth=maxdepth)
4 for rect, node in self.mbtree.nodes_by_rect.items():
5 index = node.score_index if self.isscore else node.id
6 if index > self.play.value:
7 self.ax.add_artist(patches.Rectangle(xy=(rect.x, rect.y), width=rect.width,
8 height=rect.height, fc="black", alpha=0.5))
lw=2))
略
ゲーム盤を暗く表示する処理 は先ほど draw_node
で行えるようにした ので、この処理を draw_node
で行うように修正する ことにします。どのように修正すれば良いかについて少し考えてみて下さい。
draw_subtree
の修正
上記のプログラムでは下記の手順で処理が行われます。
- 2 行目で、
draw_subtree
を呼び出してゲーム木の部分木を描画する -
draw_subtree
で描画した各ノードに対して、そのノードを暗く表示するかどうかを判定し、暗く表示する必要がある場合に半透明な正方形を上から描画して暗く表示する
手順 2 の処理を draw_board
で行うようにする ためには、draw_subtree
内 で draw_node
を呼び出す際に そのノードを暗く表示する必要があるかどうかを判定し、暗く表示する必要がある場合はキーワード引数 darkness
を記述して draw_node
を呼び出すようにする必要があります。そのためには、draw_subtree
にノードを暗く表示するかどうかを 判定するために必要なデータ を代入する 仮引数を追加 する必要があります。
ノードを暗く表示するかどうかの 判定に必要なデータ は、先程のプログラムの 5、6 行目から self.isscore
と self.play.value
の 2 つです。そこで、それらの値を代入する isscore
と anim_frame
という仮引数を draw_subtree
に追加 することにします。ただし、anim_frame
をデフォルト値を None
とするデフォルト引数とし、None
が代入されている場合は暗くする処理を行わないものとします。
draw_subtree
のブロックには、draw_node
を呼び出す処理が 4 箇所 あり、それぞれに対して仮引数 isscore
と anim_frame
を使って暗く表示するかどうかを判定する必要があります。同じ処理を 4 回も記述するのは無駄 なので、draw_subtree
の中に 表示するノードの暗さを計算する calc_darkness
というローカル関数を定義する ことにします。この関数は他の関数やメソッドから利用されることはないと思いましたので、ローカル関数としました。
下記はそのように draw_subtree
を修正したプログラムです。
-
1 行目:デフォルト値を
None
とする仮引数anim_frame
と、デフォルト値をFalse
とする 仮引数isscore
を追加する -
2 ~ 6 行目:仮引数
node
に代入されたノードを表示する暗さを計算して返り値として返すローカル関数calc_darkness
を定義する -
3、4 行目:
anim_frame
がNone
の場合は暗く表示する必要がないので、暗く表示しないことを表す0
を返り値として返す -
5、6 行目:Marubatsu_Anim クラスの
update_gui
と同じ方法でノードを暗く表示するかどうかを判定し、暗く表示する場合は0.5
を、そうでない場合は0
を返す -
9 ~ 16 行目:
draw_node
を呼び出す際に、calc_darkness
を呼び出して表示するノードの暗さを計算し、その返り値をキーワード引数darkness
に記述して呼び出すようにする
1 def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, size=0.25, lw=0.8, maxdepth=2):
2 def calc_darkness(node):
3 if anim_frame is None:
4 return 0
5 index = node.score_index if isscore else node.id
6 return 0.5 if index > anim_frame else 0
7
8 self.nodes_by_rect = {}
元と同じなので省略
9 darkness = calc_darkness(node)
10 rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
11 darkness = calc_darkness(sibling)
12 rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
元と同じなので省略
13 darkness = calc_darkness(parent)
14 rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
15 darkness = calc_darkness(node)
16 rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
17 self.nodes_by_rect[rect] = node
18
19 Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, size=0.25, lw=0.8, maxdepth=2):
def calc_darkness(node):
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")
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, size=size, lw=lw, dx=dx, dy=dy)
self.nodes_by_rect[rect] = node
dy += node.height
if len(node.children) > 0:
childnodelist += node.children
else:
childnodelist.append(None)
depth += 1
nodelist = childnodelist
if parent is not None:
dy = 0
for sibling in parent.children:
if sibling is not centernode:
sibling.height = 4
dx = 5 * sibling.depth
darkness = calc_darkness(sibling)
rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, 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, 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, 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, size=0.25, lw=0.8, maxdepth=2):
+def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, size=0.25, lw=0.8, maxdepth=2):
+ def calc_darkness(node):
+ 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 = {}
元と同じなので省略
+ darkness = calc_darkness(node)
- rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
+ rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
元と同じなので省略
+ darkness = calc_darkness(sibling)
- rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
+ rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, lw=lw, dx=dx, dy=dy)
元と同じなので省略
+ darkness = calc_darkness(parent)
- rect = parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
+ rect = parent.draw_node(ax, maxdepth=maxdepth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
元と同じなので省略
+ darkness = calc_darkness(node)
- rect = node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
+ rect = node.draw_node(ax, maxdepth=node.depth, darkness=darkness, size=size, lw=lw, dx=dx, dy=0)
self.nodes_by_rect[rect] = node
Mbtree.draw_subtree = draw_subtree
update_gui
の修正
次に、Marubatsu_Anim クラスの update_gui
を下記のプログラムのように修正します。
-
4 行目:
draw_subtree
を呼び出す際に、キーワード引数anim_frame
とisscore
を記述するように修正する - 5 行目の下にあった、ノードを暗くする処理と赤い外枠を改めて描画する処理を削除する。なお、赤い外枠を改めて描画する処理 は、元のプログラム では暗くする処理によって 赤い外枠も暗くなってしまうため行っていた が、
draw_node
では 暗く表示する処理の後で外枠を表示する ように修正したので その処理は必要がなくなっている
1 from tree import Mbtree_Anim
2
3 def update_gui(self):
元と同じなので省略
4 self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
5 anim_frame=self.play.value, isscore=self.isscore, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
6
7 Mbtree_Anim.update_gui = update_gui
行番号のないプログラム
from tree import Mbtree_Anim
def update_gui(self):
self.ax.clear()
self.ax.set_xlim(-1, self.width - 1)
self.ax.set_ylim(0, self.height)
self.ax.invert_yaxis()
self.ax.axis("off")
self.selectednode = self.nodelist[self.play.value]
self.centernode = self.selectednode
if self.mbtree.algo == "bf":
if self.centernode.depth > 0:
self.centernode = self.centernode.parent
while self.centernode.depth > 6:
self.centernode = self.centernode.parent
if self.centernode.depth <= 4:
maxdepth = self.centernode.depth + 1
elif self.centernode.depth == 5:
maxdepth = 7
else:
maxdepth = 9
self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
anim_frame=self.play.value, isscore=self.isscore, ax=self.ax, maxdepth=maxdepth)
disabled = self.play.value == 0
self.set_button_status(self.prev_button, disabled=disabled)
disabled = self.play.value == self.nodenum - 1
self.set_button_status(self.next_button, disabled=disabled)
Mbtree_Anim.update_gui = update_gui
修正箇所
from tree import Mbtree_Anim
def update_gui(self):
元と同じなので省略
self.mbtree.draw_subtree(centernode=self.centernode, selectednode=self.selectednode,
- ax=self.ax, maxdepth=maxdepth)
+ anim_frame=self.play.value, isscore=self.isscore, ax=self.ax, maxdepth=maxdepth)
- for rect, node in self.mbtree.nodes_by_rect.items():
- index = node.score_index if self.isscore else node.id
- if index > self.play.value:
- self.ax.add_artist(patches.Rectangle(xy=(rect.x, rect.y), width=rect.width,
- height=rect.height, fc="black", alpha=0.5))
- if node is self.selectednode:
- self.ax.add_artist(patches.Rectangle(xy=(rect.x, rect.y), width=rect.width,
- height=rect.height, ec="red", fill=False, lw=2))
元と同じなので省略
Mbtree_Anim.update_gui = update_gui
実行結果はいずれも省略しますが、下記のプログラムを実行して正しい処理が行われることを確認して下さい。
下記は深さ優先アルゴリズムでゲーム木が作成される様子を表示するアニメーションです。
Mbtree_Anim(mbtree)
下記は幅優先アルゴリズムでゲーム木が作成される様子を表示するアニメーションです。
bftree = Mbtree.load("../data/bftree")
Mbtree_Anim(bftree)
下記は評価値が計算される様子を表示するアニメーションです。
Mbtree_Anim(mbtree, isscore=True)
最善手以外の局面を暗く表示する処理の実装
話がかなり脱線しましたので、先程説明した 最善手を表示する手順 を再掲します。
- ノードを描画する処理を行った後で、最善手を着手した局面であるかを判定する
- 最善手を着手した局面でないと判定された場合は、以前の記事で説明した方法で、その上に半透明な黒い正方形の画像を描画することでその局面を暗く表示する
また、最善手を着手した局面であるかの判定方法 を再掲します。
- 親ノードが存在しない場合はルートノードなので最善手を着手したと判定する
-
lastmove
属性が、親ノードのbestmoves
属性の要素に含まれている場合は最善手を着手したと判定する - そうでなければ最善手を着手していないと判定する
draw_subtree
の修正
上記の処理を行うためには、最善手以外の着手が行われたノードを暗く表示することができるよう に、draw_subtree
を修正する 必要があります。そこで、下記のプログラムのように draw_subtree
に True
が代入されていた場合に最善手以外の着手が行われたノードを暗く表示する仮引数 show_bestmove
を追加 することにします。なお、下記では show_bestmove
と anim_frame
の両方が None
ではない場合は、show_bestmove
を優先することにしました。
-
2 行目:デフォルト値を
False
とする仮引数show_bestmove
を追加する -
4 ~ 10 行目:ノードを表示する暗さを計算する
cacl_darkness
の中で、show_bestmove
がTrue
の場合の暗さを計算する処理を記述する -
5、6 行目:親ノードが存在しない場合は最善手を着手したと判定し、暗く表示しないことを表す
0
を返す -
7、8 行目:直前の着手が親ノードの最善手の一覧に含まれている場合は、最善手が着手されているので
0
を返す -
9、10 行目:上記のどちらでもなければ最善手が着手されていないので、少し暗く表示することを表す
0.2
を返す。なお、anim_frame
が設定されている際に暗く表示する場合は0.5
を返していたが、0.5
だと暗すぎる感じがしたので0.2
とした。別の暗さで表示したい場合は自由に変更すること
1 def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False,
2 ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
3 def calc_darkness(node):
4 if show_bestmove:
5 if node.parent is None:
6 return 0
7 elif node.mb.last_move in node.parent.bestmoves:
8 return 0
9 else:
10 return 0.2
11 if anim_frame is None:
12 return 0
13 index = node.score_index if isscore else node.id
14 return 0.5 if index > anim_frame else 0
元と同じなので省略
15
16 Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False,
ax=None, show_bestmove=False, 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")
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, size=size, lw=lw, dx=dx, dy=dy)
self.nodes_by_rect[rect] = node
dy += node.height
if len(node.children) > 0:
childnodelist += node.children
else:
childnodelist.append(None)
depth += 1
nodelist = childnodelist
if parent is not None:
dy = 0
for sibling in parent.children:
if sibling is not centernode:
sibling.height = 4
dx = 5 * sibling.depth
darkness = calc_darkness(sibling)
rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, darkness=darkness, 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, 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, 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, anim_frame=None, isscore=False,
- ax=None, size=0.25, lw=0.8, maxdepth=2):
+ ax=None, show_bestmove=False, 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
元と同じなので省略
Mbtree.draw_subtree = draw_subtree
update_gui
メソッドの修正
次に、Mbtree_GUI クラスの update_gui
の中で、下記のプログラムの 3 行目のように draw_subtree
を呼び出す際に show_bestmove=True
を記述するように修正します。
1 def update_gui(self):
元と同じなので省略
2 self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
3 show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
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(0, self.height)
self.ax.invert_yaxis()
self.ax.axis("off")
if self.selectednode.depth <= 4:
maxdepth = self.selectednode.depth + 1
elif self.selectednode.depth == 5:
maxdepth = 7
else:
maxdepth = 9
centernode = self.selectednode
while centernode.depth > 6:
centernode = centernode.parent
self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
disabled = self.selectednode.parent is None
self.set_button_status(self.left_button, disabled=disabled)
disabled = self.selectednode.depth >= 6 or len(self.selectednode.children) == 0
self.set_button_status(self.right_button, disabled=disabled)
disabled = self.selectednode.parent is None or self.selectednode.parent.children.index(self.selectednode) == 0
self.set_button_status(self.up_button, disabled=disabled)
disabled = self.selectednode.parent is None or self.selectednode.parent.children[-1] is self.selectednode
self.set_button_status(self.down_button, disabled=disabled)
Mbtree_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
- ax=self.ax, maxdepth=maxdepth)
+ show_bestmove=True, ax=self.ax, maxdepth=maxdepth)
元と同じなので省略
Mbtree_GUI.update_gui = update_gui
上記の修正後に下記のプログラムを実行し、別のノードをクリックして選択する4と実行結果の左図ように最善手が着手されていない局面のノードが暗く表示されます。また、実行結果の右図では過去の 3 手分の着手がいずれも最善手ではないことがわかるようになります。
gui_play()
実行結果(GUI の部分木のみを表示します)
以上で、最善手を明確にする処理の実装は完了です。
最善手のみを着手した場合の部分木の表示
前回の記事では、将棋や囲碁の中継などで、プロ棋士の対戦中の局面に対して下図の上部のような「現在の局面から数手先までの最善手の一覧」という工夫がされているという話をしました。そこで、〇×ゲームでも、GUI の部分木に同様の表示を行う ことにします。どのように表示を行えば良いかについて少し考えてみて下さい。
表示の仕様
現状の GUI の部分木では、現在の局面の 深さが浅い場合 は、下図のように現在の局面の 次の深さのノードまでしか表示されません が、その後に そこから決着がつくまで 最善手のみを着手した局面を表示する という方法が考えられます。
ピンとこない人が多いのではないかと思いますので、完成版の GUI の部分木の表示例を下図に示します。下図では、ゲーム開始時の局面に対する 9 つの それぞれの合法手を着手した局面 に対して、その 右の背景が灰色になっている部分 に 最善手を着手し続けた場合の局面 を並べて表示しています。このような表示を行うことで、それぞれの合法手を着手した後 の 試合の展開と結果が一目でわかる ようになります。
なお、先程の図の将棋というゲームは決着がつくまでに平均約 100 手程かかるので、3 手先までの最善手の一覧を表示していますが、〇×ゲームは最大でも 9 手までなので、ゲームの決着がつくまでの最善手のを着手した局面の一覧を表示 することにしました。
また、最善手が複数ある場合 は、bestmoves
属性の 最初の最善手のみ を着手することにします。そうすることで、着手の組み合わせ が必ず 1 通りになる ので、最善手のみを着手した局面を表示しても、GUI の部分木の 縦幅が広がることはありません。
上記の処理をどのように実装すればよいかについて少し考えてみて下さい。
draw_subtree
の修正
GUI の部分木 は draw_subtree
メソッドによって描画を行います。その中で 中心となる centernode
から maxdepth
までの部分木を表する という処理を行っていますが、その際に maxdepth
の深さの子ノードを描画した後 で、ゲームの決着がつくまで最善手を着手したノードを表示し続ける という処理を記述する必要があります。また、これまでと同様の方法で部分木を表示できるようにする ために、その処理は先ほど追加した 仮引数 show_bestmove
に True
が代入されている場合に行う ことにします。
下記はそのように draw_subtree
を修正したプログラムです。
-
4 ~ 12 行目:2 行目で
node
に代入されたノードをdraw_node
で描画した後で、show_bestmove
にTrue
が代入されており、node
の深さがmaxdepth
と等しい場合にnode
から最善手のみを着手した場合の局面を表示する処理を行う -
5 行目:
node
に代入された値 は、13 行目のように この処理が行われた後で利用する ので、node
の値をbestnode
という変数に代入 し、その変数を使って処理を行う -
6 行目:
bestnode
の最善手を表すbestmoves
属性に要素が存在する間繰り返し処理を行う。ゲーム中であるばあいは最善手が必ず存在することから、この条件式をbestnode.mb.status == Marubatsu.PLAYING
としても良い -
7、8 行目:
bestmoves
属性の最初の要素を着手する最善手とし、children_by_move
属性を利用してその最善手を着手した子ノードを計算してbestnode
に代入する -
9 行目:
bestnode
を表示する位置の x 座標をノードの深さから計算する -
10 行目:
draw_subtree
では、最初にself.calc_node_height(N=centernode, maxdepth=maxdepth)
を実行することで、深さmaxdepth
までのノードの表示の高さを計算している が、これから表示するbestnode
の深さはmaxdepth
よりも深い のでheight
属性の計算は行っていない。そのため、height
属性 に表示するノードの高さを表す4
を代入しておく必要がある5点に注意すること -
11 行目:
draw_node
メソッドを使ってbestnode
を表示する。その際に、maxdepth
にbestnode
の深さを代入することでbestnode
に子ノードが存在する場合に右に 1 本だけ線を描画するようにする。また、bestnode
は最善手を着手した局面 なので、暗く表示する必要はない ので、実引数darkness
を記述する必要はない -
12 行目:
nodes_by_rect
属性に代入された dict にbestnode
の情報を登録する6
1 def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
2 rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, darkness=darkness, size=size, lw=lw, dx=dx, dy=dy)
3 self.nodes_by_rect[rect] = node
4 if show_bestmove and depth == maxdepth:
5 bestnode = node
6 while len(bestnode.bestmoves) > 0:
7 bestmove = bestnode.bestmoves[0]
8 bestnode = bestnode.children_by_move[bestmove]
9 dx = 5 * bestnode.depth
10 bestnode.height = 4
11 rect = bestnode.draw_node(ax=ax, maxdepth=bestnode.depth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
12 self.nodes_by_rect[rect] = bestnode
13 dy += node.height
元と同じなので省略
13
14 Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, 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")
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, 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, 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, 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, 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, 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, anim_frame=None, isscore=False, ax=None, show_bestmove=False, 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)
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, size=size, lw=lw, dx=dx, dy=dy)
+ self.nodes_by_rect[rect] = bestnode
dy += node.height
元と同じなので省略
Mbtree.draw_subtree = draw_subtree
上記の修正後に下記のプログラムを実行すると、実行結果の左図のように、ゲーム開始時の局面のそれぞれの子ノードの後ろに、決着がつくまで最善手を着手し続けた場合の局面が表示されるようになります。右図は、別のノードをクリックして選択した場合のものです。最善手である (1, 1) を着手した場合に引き分けになる着手の例と、それ以外を着手した場合に 〇 が勝利する着手の例が表示されます。
gui_play()
実行結果(GUI の部分木のみを表示します)
最善手を着手した局面の区別
上図では どこからが最善手を着手し続けた局面であるかがわかりづらい という欠点があります。そこで、最善手を着手し続けた局面 の部分の 背景色を灰色で表示 することで 区別できるようにする という工夫を行うことにします。
下記はそのように draw_subtree
を修正したプログラムです。
-
2 ~ 6 行目:
show_bestmove
がTrue
の場合に、maxdepth
より深いノードの部分の背景色を灰色で表示するようにする -
3 行目:灰色の長方形の x 座標を計算して
bestx
に代入する。この式は、深さmaxdepth
のノードの x 座標を表す5 * maxdepth
に様々な数値を足した式を記述してプログラムを実行して表示を確認するという、試行錯誤によって決めた式 である -
4 行目:Mbtree_GUI では、Figure の幅を 50 に設定している ので、灰色の長方形の表示幅は 50 から
bestx
を引いた値で計算できる -
5、6 行目:Figure の中で、
bestx
から右の長方形の範囲を灰色で塗りつぶす
1 def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
2 if show_bestmove:
3 bestx = 5 * maxdepth + 4
4 bestwidth = 50 - bestx
5 ax.add_artist(patches.Rectangle(xy=(bestx, 0), width=bestwidth,
6 height=height, fc="lightgray"))
7
8 nodelist = [centernode]
9 depth = centernode.depth
10 while len(nodelist) > 0 and depth <= maxdepth:
元と同じなので省略
11
12 Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, selectednode=None, anim_frame=None, isscore=False, ax=None, show_bestmove=False, 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, 0), width=bestwidth,
height=height, 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, 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, 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, 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, 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, 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, anim_frame=None, isscore=False, ax=None, show_bestmove=False, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
+ if show_bestmove:
+ bestx = 5 * maxdepth + 4
+ bestwidth = 50 - bestx
+ ax.add_artist(patches.Rectangle(xy=(bestx, 0), width=bestwidth,
+ height=height, fc="lightgray"))
nodelist = [centernode]
depth = centernode.depth
while len(nodelist) > 0 and depth <= maxdepth:
元と同じなので省略
Mbtree.draw_subtree = draw_subtree
上記の修正後に下記のプログラムを実行すると、実行結果の左図のように、最善手を着手し続けた部分の背景色が灰色で表示されるようになります。右図は別の局面を中心とした場合の図です。
gui_play()
実行結果(GUI の部分木のみを表示します)
これで、GUI のゲーム木から以下の事が簡単にわかるようになりました。
- 現在の局面に対する 合法手の中の最善手
- それぞれの合法手を着手した局面から、最善手を着手し続けた場合の決着までの経過
ヘルプの表示の ON/OFF
現状では、〇×ゲームの GUI の ? ボタンのヘルプ に関して 下記のような問題 があります。
- ヘルプを表示すると、ゲームをリセットするまでヘルプの表示が消えない
- ヘルプの表示 と、既にマークが配置されたマスをクリックした際の メッセージの表示 が 同じ場所に行われる
また、また、GUI の部分木 の ? ボタンの場合は、一度クリックすると ヘルプの表示が二度と消えなくなる という問題があります。
そこで、? ボタン によって、ヘルプの表示の有無を切り替える ことができるように修正することにします。表示の切り替えは、前回の記事で説明したように、ウィジェットの layout.display
属性を利用することで行えます。
Marubatsu_GUI クラスのヘルプの表示の修正
まず、ヘルプの表示と、既にマークが配置されたマスをクリックした際のメッセージの 表示を分けるため に、下記のプログラムのように create_widgets
を修正します。
-
8 行目:ヘルプを表示する Output を作成し、
help
属性に代入する -
9 行目:ヘルプの表示の有無は、
layout.display
属性を使って切り替えることにしたので、ヘルプのメッセージは最初に表示しておけばよい。ヘルプに表示するメッセージを修正するたびにcreate_widgets
メソッドを修正するのは面倒なので、メッセージを表示するprint_helpmessage
メソッドをこの後で定義 し、そのメソッドを呼び出すことにする -
10 行目:
layout.display
属性に"none"
を代入して 最初はヘルプを非表示状態にする
1 import ipywidgets as widgets
2
3 def create_widgets(self):
元と同じなので省略
4 # print による文字列を表示する Output を作成する
5 self.output = widgets.Output()
6
7 # ヘルプを表示する Output を作成し、表示の設定を行う
8 self.help = widgets.Output()
9 self.print_helpmessage()
10 self.help.layout.display = "none"
11
12 Marubatsu_GUI.create_widgets = create_widgets
行番号のないプログラム
import ipywidgets as widgets
def create_widgets(self):
# 乱数の種の Checkbox と IntText を作成する
self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
indent=False, layout=widgets.Layout(width="100px"))
self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
layout=widgets.Layout(width="80px"))
# 読み書き、ヘルプのボタンを作成する
self.load_button = self.create_button("開く", 50)
self.save_button = self.create_button("保存", 50)
self.show_tree_button = self.create_button("木", 34)
self.reset_tree_button = self.create_button("リ", 34)
self.help_button = self.create_button("?", 34)
# AI を選択する Dropdown を作成する
self.create_dropdown()
# 変更、リセット、待ったボタンを作成する
self.change_button = self.create_button("変更", 50)
self.reset_button = self.create_button("リセット", 80)
self.undo_button = self.create_button("待った", 60)
# リプレイのボタンとスライダーを作成する
self.first_button = self.create_button("<<", 50)
self.prev_button = self.create_button("<", 50)
self.next_button = self.create_button(">", 50)
self.last_button = self.create_button(">>", 50)
self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
# ゲーム盤の画像を表す figure を作成する
self.create_figure()
# print による文字列を表示する Output を作成する
self.output = widgets.Output()
# ヘルプを表示する Output を作成し、表示の設定を行う
self.help = widgets.Output()
self.print_helpmessage()
self.help.layout.display = "none"
Marubatsu_GUI.create_widgets = create_widgets
修正箇所
import ipywidgets as widgets
def create_widgets(self):
元と同じなので省略
# print による文字列を表示する Output を作成する
self.output = widgets.Output()
# ヘルプを表示する Output を作成し、表示の設定を行う
+ self.help = widgets.Output()
+ self.print_helpmessage()
+ self.help.layout.display = "none"
Marubatsu_GUI.create_widgets = create_widgets
print_helpmessage
メソッドの定義
次に、下記のプログラムのように print_helpmessage
メソッドを定義します。行う処理は、with self.output
を with self.help
に変更した以外は create_eventhandler
内でヘルプを表示する処理と同じです。なお、下記のプログラムには、前回の記事で追加した「木」と「リ」ボタンの説明を追記しました。
def print_helpmessage(self):
with self.help:
print("""操作説明
マスの上でクリックすることで着手を行う。
下記の GUI で操作を行うことができる。
()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。
乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
開く(-,L)\tファイルから対戦データを読み込む
保存(+,S)\tファイルに対戦データを保存する
木\t下部の GUI の部分木を表示の有無を切り替える
リ\r株の GUI の部分木の中心となるノードを、現在の局面にリセットする
?(*,H)\t\tこの操作説明を表示する
手番の担当\tメニューからそれぞれの手番の担当を選択する
\t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
変更\t\tゲームの途中で手番の担当を変更する
リセット\t手番の担当を変更してゲームをリセットする
待った(0)\t1つ前の自分の着手をキャンセルする
<<(↑)\t\t最初の局面に移動する
<(←)\t\t1手前の局面に移動する
>(→)\t\t1手後の局面に移動する
>>(↓)\t\t最後の着手が行われた局面に移動する
スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する
手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
Marubatsu_GUI.print_helpmessage = print_helpmessage
display_widgets
の修正
次に、display_widgets
を下記のプログラムのように修正します。
-
3 行目:VBox の最後に
self.help
を配置するように修正する
1 def display_widgets(self):
元と同じなので省略
2 # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
3 display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output, self.help]))
4
5 Marubatsu_GUI.display_widgets = display_widgets
行番号のないプログラム
def display_widgets(self):
# 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button,
self.show_tree_button, self.reset_tree_button, self.help_button])
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider])
# hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output, self.help]))
Marubatsu_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):
元と同じなので省略
# hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output, self.help]))
Marubatsu_GUI.display_widgets = display_widgets
create_event_handler
の修正
最後に、create_event_handler
内で ? ボタンをクリックした際のイベントハンドラを下記のプログラムのように修正します。
- 5 行目:前回の記事で GUI の部分木の表示の有無を切り替えたのと同じ方法で、ヘルプメッセージの Ouput の表示を切り替える処理を記述する
- 5 行目の下にあった、ヘルプの Output の表示内容をクリアする処理と、ヘルプメッセージを表示する処理は必要が無くなったので削除する
1 import math
2
3 def create_event_handler(self):
元と同じなので省略
4 def on_help_button_clicked(b=None):
5 self.help.layout.display = "none" if self.help.layout.display is None else None
元と同じなので省略
6
7 Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
import math
def create_event_handler(self):
# 乱数の種のチェックボックスのイベントハンドラを定義する
def on_checkbox_changed(changed):
self.update_widgets_status()
self.checkbox.observe(on_checkbox_changed, names="value")
# 開く、保存ボタンのイベントハンドラを定義する
def on_load_button_clicked(b=None):
path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
initialdir="save")
if path != "":
with open(path, "rb") as f:
data = pickle.load(f)
self.mb.records = data["records"]
self.mb.ai = data["ai"]
self.params = data["params"] if "params" in data else [ {}, {} ]
if "names" in data:
names = data["names"]
else:
names = [ "人間" if mb.ai[i] is None else mb.ai[i].__name__ for i in range(2)]
options = self.dropdown_list[0].options.copy()
for i in range(2):
value = (self.mb.ai[i], self.params[i])
if not value in options.values():
options[names[i]] = value
for i in range(2):
self.dropdown_list[i].options = options
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
change_step(data["move_count"])
if data["seed"] is not None:
self.checkbox.value = True
self.inttext.value = data["seed"]
else:
self.checkbox.value = False
def on_save_button_clicked(b=None):
names = [ self.dropdown_list[i].label for i in range(2) ]
timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
fname = f"{names[0]} VS {names[1]} {timestr}"
path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
initialdir="save", initialfile=fname,
defaultextension="mbsav")
if path != "":
with open(path, "wb") as f:
data = {
"records": self.mb.records,
"move_count": self.mb.move_count,
"ai": self.mb.ai,
"params": self.params,
"names": names,
"seed": self.inttext.value if self.checkbox.value else None
}
pickle.dump(data, f)
def on_show_tree_button_clicked(b=None):
self.mbtree_gui.vbox.layout.display = "none" if self.mbtree_gui.vbox.layout.display is None else None
def on_reset_tree_button_clicked(b=None):
self.update_gui()
def on_help_button_clicked(b=None):
self.help.layout.display = "none" if self.help.layout.display is None else None
self.load_button.on_click(on_load_button_clicked)
self.save_button.on_click(on_save_button_clicked)
self.show_tree_button.on_click(on_show_tree_button_clicked)
self.reset_tree_button.on_click(on_reset_tree_button_clicked)
self.help_button.on_click(on_help_button_clicked)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
self.mb.play_loop(self, self.params)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b=None):
# 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
if self.checkbox.value:
random.seed(self.inttext.value)
self.mb.restart()
self.output.clear_output()
on_change_button_clicked(b)
# 待ったボタンのイベントハンドラを定義する
def on_undo_button_clicked(b=None):
if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
self.mb.move_count -= 2
self.mb.records = self.mb.records[0:self.mb.move_count+1]
self.mb.change_step(self.mb.move_count)
self.update_gui()
# イベントハンドラをボタンに結びつける
self.change_button.on_click(on_change_button_clicked)
self.reset_button.on_click(on_reset_button_clicked)
self.undo_button.on_click(on_undo_button_clicked)
# step 手目の局面に移動する
def change_step(step):
self.mb.change_step(step)
# 描画を更新する
self.update_gui()
def on_first_button_clicked(b=None):
change_step(0)
def on_prev_button_clicked(b=None):
change_step(self.mb.move_count - 1)
def on_next_button_clicked(b=None):
change_step(self.mb.move_count + 1)
def on_last_button_clicked(b=None):
change_step(len(self.mb.records) - 1)
def on_slider_changed(changed):
if self.mb.move_count != changed["new"]:
change_step(changed["new"])
self.first_button.on_click(on_first_button_clicked)
self.prev_button.on_click(on_prev_button_clicked)
self.next_button.on_click(on_next_button_clicked)
self.last_button.on_click(on_last_button_clicked)
self.slider.observe(on_slider_changed, names="value")
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.mb.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
with self.output:
self.mb.move(x, y)
# 次の手番の処理を行うメソッドを呼び出す
self.mb.play_loop(self, self.params)
# ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
def on_key_press(event):
keymap = {
"up": on_first_button_clicked,
"left": on_prev_button_clicked,
"right": on_next_button_clicked,
"down": on_last_button_clicked,
"0": on_undo_button_clicked,
"enter": on_reset_button_clicked,
"-": on_load_button_clicked,
"l": on_load_button_clicked,
"+": on_save_button_clicked,
"s": on_save_button_clicked,
"*": on_help_button_clicked,
"h": on_help_button_clicked,
}
if event.key in keymap:
keymap[event.key]()
else:
try:
num = int(event.key) - 1
event.inaxes = True
event.xdata = num % 3
event.ydata = 2 - (num // 3)
on_mouse_down(event)
except:
pass
# fig の画像イベントハンドラを結び付ける
self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)
self.fig.canvas.mpl_connect("key_press_event", on_key_press)
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
import math
def create_event_handler(self):
元と同じなので省略
def on_help_button_clicked(b=None):
+ self.help.layout.display = "none" if self.help.layout.display is None else None
- self.output.clear_output()
- with self.output:
- print("""操作説明
- 以下の表示の部分は長いので省略
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
実行結果は省略しますが、下記のプログラムを実行して ? ボタンをクリックすることで、ヘルプの表示の切り替えを行うことができるようになったことを確認して下さい。また、既にマークが配置されたマスをクリックした際のメッセージが、ヘルプとは別の Output に表示されるようになったことも確認して下さい。
gui_play()
Mbtree_GUI のヘルプの修正
Mbtree_GUI のヘルプも同様の方法で修正することができます。行う処理は上記と同様なので、修正したプログラムのみを下記に記述します。なお、self.output
を self.help
に修正すると、修正箇所が増えてしまうので、self.output
のままとしました。
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.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 print_helpmessage(self):
with self.output:
print("""操作説明
下記のキーとボタンで中心となるノードを移動できる。
←、0 キー:親ノードへ移動
↑:一つ前の兄弟ノードへ移動
↓:一つ後の兄弟ノードへ移動
→:先頭の子ノードへ移動
テンキーで、対応するマスに着手が行われた子ノードへ移動する
ノードの上でマウスを押すことでそのノードへ移動する
背景が灰色になっている部分のノードは、最善手を着手し続けた場合のノードを表す
""")
Mbtree_GUI.print_helpmessage = print_helpmessage
def create_event_handler(self):
def on_left_button_clicked(b=None):
if self.selectednode.parent is not None:
self.selectednode = self.selectednode.parent
self.update_gui()
def on_right_button_clicked(b=None):
if len(self.selectednode.children) > 0:
self.selectednode = self.selectednode.children[0]
self.update_gui()
def on_up_button_clicked(b=None):
if self.selectednode.parent is not None:
index = self.selectednode.parent.children.index(self.selectednode)
if index > 0:
self.selectednode = self.selectednode.parent.children[index - 1]
self.update_gui()
def on_down_button_clicked(b=None):
if self.selectednode.parent is not None:
index = self.selectednode.parent.children.index(self.selectednode)
if self.selectednode.parent.children[-1] is not self.selectednode:
self.selectednode = self.selectednode.parent.children[index + 1]
self.update_gui()
def on_help_button_clicked(b=None):
self.output.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.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
実行結果は省略しますが、下記のプログラムを実行して GUI の部分木の ? ボタンをクリックすることで、ヘルプの表示の切り替えを行うことができるようになったことを確認して下さい。
gui_play()
今回の記事のまとめ
今回の記事では、最初に循環参照の問題を解決する方法を紹介しました。
次に、合法手の中の最善手がわかるように GUI の部分木の表示を修正し、合法手を着手した局面から最善手を着手し続けた場合の局面を表示するように修正しました。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu_new.py | 今回の記事で更新した marubatsu.py |
tree_new.py | 今回の記事で更新した tree.py |
次回の記事
-
前回の記事で、ゲーム盤の下に Mbtree_GUI クラスを使って表示する部分木のことを、GUI の部分木 と表記することにしました ↩
-
matplotlib では線(edge(辺))の色を
edgecolor
またはec
という名前の仮引数に代入しますが、〇×ゲームには外枠の線(border(境界線))だけでなく、ゲーム盤の線も存在するので bordercolor の略のbc
としました ↩ -
ゲーム盤の線の太さを表す仮引数
lw
が既に存在するので、こちらは borderwidth の略のbw
としました ↩ -
ゲーム開始時の局面は、すべての合法手が最善手なのでこれまでと表示がかわりません。そのため、別のノードを選択しました ↩
-
筆者もこの処理を最初は記述し忘れてしまい、エラーが発生しました ↩
-
この処理は、ノードをクリックすることでそのノードを中心となるノードに移動する処理を行うためのものです ↩