目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
評価値が計算された部分木の動的な作成と表示
今回の記事も前回に引き続き、draw_subtree
メソッドで表示する 部分木を動的に作成する処理 の実装の続きを行います。
現状では、create_subtree
で作成した部分木を draw_subtree
で表示した際に、評価値が表示されない という問題があります。draw_subtree
で評価値を表示するためには、部分木の それぞれのノードの score
属性に評価値を記録 しておく必要がありますが、そのデータは create_subtree
で部分木を作成する際に利用する、局面と最善手の対応表を表す bestmoves_by_board.dat
には保存されていません。
そこで、局面と最善手の対応表のデータに、局面と評価値の対応表のデータを加えた ものを作製し、それを使って create_subtree
で 評価値を計算した部分木を作成 するように修正することにします。そのようなデータを以降は「局面と最善手・評価値の対応表のデータ」と表記することにします。
局面と最善手・評価値の対応表のデータの作成
局面と評価値の対応表のデータは、util.py の中で定義した calc_and_save_bestmoves_by_board
という関数で作成し、ファイルに保存していました。局面と最善手・評価値の対応表のデータを作成してファイルに保存する処理は、この関数と同様の処理 で行うことができます。
局面と最善手・評価値の対応表のデータ構造
まず、局面と最善手・評価値の対応表のデータを どのようなデータ構造で表現するか を決める必要があります。どのようなデータ構造にすれば良いかについて少し考えてみて下さい。
局面と最善手の対応表のデータは、下記のような dict で表現 しました。
- キーを局面を表す文字列とする
- キーの値をその局面の最善手の一覧を表す list で表現する
同様に、局面と最善手・評価値の対応表のデータは、下記のような dict で表現することができます。
- キーを局面を表す文字列とする
- キーの値をその局面の「最善手の一覧を表す list」と「評価値」を表すデータとする
「最善手の一覧表す list」と「評価値」をまとめて表現する方法としては、以下のような方法が考えられます。
- 0 番の要素を最善手の一覧表す list、1 番の要素を評価値とする tuple(または list)で表現 する
-
bestmoves
というキーの値に最善手の一覧を表す list を、score
というキーの値に評価値を代入する dict で表現 する
どちらの方法を使っても構いませんが、前者はプログラムの記述が短くなる、後者はプログラムがわかりやすくなるという利点があります。本記事ではわかりやすさを重視して後者の方法を採用することにします。なお、上記以外の方法で表現しても構わないので、よりよい方法を思いついた方はその方法で実装してみて下さい。
calc_and_save_bestmoves_by_board
のメソッドとしての定義
局面と最善手・評価値の対応表のデータを作成してファイルに保存する処理を実装する前に、局面と最善手の対応表のデータ を作成してファイルに保存する処理を行う、calc_and_save_bestmoves_by_board
に関する 修正を行う ことにします。
以前の記事ではこの処理を 通常の関数として定義 しましたが、よく考えるとこの処理は Mbtree クラスのインスタンスに対して行う処理 なので、Mbtree クラスの メソッドとして定義したほうが自然 です。そこで、下記のプログラムのように、Mbtree クラスのメソッドとして calc_and_save_bestmoves_by_board
を 定義する事にします。
-
6 行目:仮引数
mbtree
をself
に修正する -
8 行目:
mbtree
をself
に修正する
1 from tree import Mbtree
2 import pickle
3 import gzip
4 from tqdm import tqdm
5
6 def calc_and_save_bestmoves_by_board(self, path):
7 bestmoves_by_board = {}
8 for node in tqdm(self.nodelist):
9 txt = node.mb.board_to_str()
10 if not txt in bestmoves_by_board.keys():
11 bestmoves_by_board[txt] = node.bestmoves
12
13 with gzip.open(path, "wb") as f:
14 pickle.dump(bestmoves_by_board, f)
15
16 return bestmoves_by_board
17
18 Mbtree.calc_and_save_bestmoves_by_board = calc_and_save_bestmoves_by_board
行番号のないプログラム
from tree import Mbtree
import pickle
import gzip
from tqdm import tqdm
def calc_and_save_bestmoves_by_board(self, path):
bestmoves_by_board = {}
for node in tqdm(self.nodelist):
txt = node.mb.board_to_str()
if not txt in bestmoves_by_board.keys():
bestmoves_by_board[txt] = node.bestmoves
with gzip.open(path, "wb") as f:
pickle.dump(bestmoves_by_board, f)
return bestmoves_by_board
Mbtree.calc_and_save_bestmoves_by_board = calc_and_save_bestmoves_by_board
修正箇所
from tree import Mbtree
import pickle
import gzip
from tqdm import tqdm
-def calc_and_save_bestmoves_by_board(mbtree, path):
+def calc_and_save_bestmoves_by_board(self, path):
bestmoves_by_board = {}
- for node in tqdm(mbtree.nodelist):
+ for node in tqdm(self.nodelist):
txt = node.mb.board_to_str()
if not txt in bestmoves_by_board.keys():
bestmoves_by_board[txt] = node.bestmoves
with gzip.open(path, "wb") as f:
pickle.dump(bestmoves_by_board, f)
return bestmoves_by_board
Mbtree.calc_and_save_bestmoves_by_board = calc_and_save_bestmoves_by_board
なお、util.py に記述していた calc_and_save_bestmoves_by_board
はもう必要がなくなったので削除することにします。
calc_and_save_bestmoves_and_score_by_board
メソッドの定義
次に 局面と最善手・評価値の対応表のデータ を作成してファイルに保存するメソッドを定義します。メソッドの名前は calc_and_save_bestmoves_and_score_by_board
とし、下記のプログラムのように定義します。
なお、メソッドの 名前が長すぎる と思った方は、簡略化した名前を付けてもかまいませんが、簡略化しすぎるとプログラムがわかりづらくなってしまう 点に注意して下さい。本記事ではわかりやすさを重視して長い名前を採用することにします。
-
1 行目:メソッドを定義する。仮引数は
calc_and_save_bestmoves_by_board
と同じ -
2 行目:作成したデータを記録する変数名を
bestmoves_and_score_by_board
とする -
5、6、12 行目:
bestmoves_by_board
をbestmoves_and_score_by_board
に修正する - 6 ~ 9 行目:最善手の一覧と評価値のデータを先程説明した dict の形式で代入する
1 def calc_and_save_bestmoves_and_score_by_board(self, path):
2 bestmoves_and_score_by_board = {}
3 for node in tqdm(self.nodelist):
4 txt = node.mb.board_to_str()
5 if not txt in bestmoves_and_score_by_board.keys():
6 bestmoves_and_score_by_board[txt] = {
7 "bestmoves": node.bestmoves,
8 "score": node.score,
9 }
10
11 with gzip.open(path, "wb") as f:
12 pickle.dump(bestmoves_and_score_by_board, f)
13
14 return bestmoves_and_score_by_board
15
16 Mbtree.calc_and_save_bestmoves_and_score_by_board = calc_and_save_bestmoves_and_score_by_board
行番号のないプログラム
def calc_and_save_bestmoves_and_score_by_board(self, path):
bestmoves_and_score_by_board = {}
for node in tqdm(self.nodelist):
txt = node.mb.board_to_str()
if not txt in bestmoves_and_score_by_board.keys():
bestmoves_and_score_by_board[txt] = {
"bestmoves": node.bestmoves,
"score": node.score,
}
with gzip.open(path, "wb") as f:
pickle.dump(bestmoves_and_score_by_board, f)
return bestmoves_and_score_by_board
Mbtree.calc_and_save_bestmoves_and_score_by_board = calc_and_save_bestmoves_and_score_by_board
修正箇所
-def calc_and_save_bestmoves_by_board(self, path):
+def calc_and_save_bestmoves_and_score_by_board(self, path):
- bestmoves_by_board = {}
+ bestmoves_and_score_by_board = {}
for node in tqdm(self.nodelist):
txt = node.mb.board_to_str()
- if not txt in bestmoves_by_board.keys():
+ if not txt in bestmoves_and_score_by_board.keys():
- bestmoves_by_board[txt] = node.bestmoves
+ bestmoves_and_score_by_board[txt] = {
+ "bestmoves": node.bestmoves,
+ "score": node.score,
+ }
with gzip.open(path, "wb") as f:
- pickle.dump(bestmoves_by_board, f)
+ pickle.dump(bestmoves_and_score_by_board, f)
return bestmoves_and_score_by_board
Mbtree.calc_and_save_bestmoves_and_score_by_board = calc_and_save_bestmoves_and_score_by_board
上記を実行後に、下記のプログラムを実行して、下記の 2 種類のゲーム木のデータをファイルから読み込み、局面と最善手・評価値の対応表を作成してファイルに保存します。
- 通常の方法で評価値を計算した
aidata.mbtree
。bestmoves_and_score_by_board.dat という名前のファイルに保存する -
最短の勝利を優先した評価値を計算した
bftree_shortest_victory.mbtree
。bestmoves_and_score_by_board_shortest_victory.dat という名前のファイルに保存する
mbtree = Mbtree.load("../data/aidata")
bestmoves_and_score_by_board = mbtree.calc_and_save_bestmoves_and_score_by_board(
"../data/bestmoves_and_score_by_board.dat")
bftree_shortest_victory = Mbtree.load("../data/bftree_shortest_victory")
bestmoves_and_score_by_board_shortest_victory = \
bftree_shortest_victory.calc_and_save_bestmoves_and_score_by_board(
"../data/bestmoves_and_score_by_board_shortest_victory.dat")
実行結果
100%|██████████| 549946/549946 [00:00<00:00, 593251.04it/s]
100%|██████████| 549946/549946 [00:01<00:00, 456474.54it/s]
なお、作成されたファイルは、どちらも約 23 KB のように小さいので、ファイルから読み込む際に時間はほとんどかかりません。
ファイル名が長すぎると思った方は、もっと短い名前でデータ保存してもかまいませんが、メソッドの名前と同様に、簡略化しすぎると後からそのファイルを見た時にどのようなデータが記録されているかがわかりづらくなる点に注意して下さい。
Node クラスの __init__
メソッドの修正
下記のプログラムのように Node クラスの __init__
メソッドを修正することで、ノードの作成時 に 評価値を計算して score
属性に代入 することができます。なお、この修正は前回の記事で ノードの作成時に最善手を計算して bestmoves
属性に代入するようにした際の修正とほぼ同じです。
-
3 行目:仮引数
bestmoves_by_board
をbestmoves_and_score_by_board
に修正する -
11 行目:
bestmoves_by_board
をbestmoves_and_score_by_board
に修正する -
12 ~ 14 行目:ゲーム盤を文字列に変換した値を利用して最善手の一覧と評価値が代入されたキーの値を取り出し、それぞれの値を
bestmoves
属性とscore
属性に代入する
1 from tree import Node
2
3 def __init__(self, mb, parent=None, depth=0, bestmoves_and_score_by_board=None):
4 self.id = Node.count
5 Node.count += 1
6 self.mb = mb
7 self.parent = parent
8 self.depth = depth
9 self.children = []
10 self.children_by_move = {}
11 if bestmoves_and_score_by_board is not None:
12 bestmoves_and_score = bestmoves_and_score_by_board[self.mb.board_to_str()]
13 self.bestmoves = bestmoves_and_score["bestmoves"]
14 self.score = bestmoves_and_score["score"]
15
16 Node.__init__ = __init__
行番号のないプログラム
from tree import Node
def __init__(self, mb, parent=None, depth=0, bestmoves_and_score_by_board=None):
self.id = Node.count
Node.count += 1
self.mb = mb
self.parent = parent
self.depth = depth
self.children = []
self.children_by_move = {}
if bestmoves_and_score_by_board is not None:
bestmoves_and_score = bestmoves_and_score_by_board[self.mb.board_to_str()]
self.bestmoves = bestmoves_and_score["bestmoves"]
self.score = bestmoves_and_score["score"]
Node.__init__ = __init__
修正箇所
from tree import Node
-def __init__(self, mb, parent=None, depth=0, bestmoves_by_board=None):
+def __init__(self, mb, parent=None, depth=0, bestmoves_and_score_by_board=None):
self.id = Node.count
Node.count += 1
self.mb = mb
self.parent = parent
self.depth = depth
self.children = []
self.children_by_move = {}
- if bestmoves_by_board is not None:
+ if bestmoves_and_score_by_board is not None:
- self.bestmoves = bestmoves_by_board[self.mb.board_to_str()]
+ bestmoves_and_score = bestmoves_and_score_by_board[self.mb.board_to_str()]
+ self.bestmoves = bestmoves_and_score["bestmoves"]
+ self.score = bestmoves_and_score["score"]
Node.__init__ = __init__
Node クラスの calcchildren
メソッドの修正
前回の記事と同様に、Node クラスの __init__
メソッドの修正にあわせて Node クラスのインスタンスを作成する処理を修正 する必要があります。まず、子ノードの一覧を作成する Node クラスの calc_children
メソッドを下記のプログラムのように修正します。
-
3 行目:仮引数
bestmoves_by_board
をbestmoves_and_score_by_board
に修正する -
9 行目:
bestmoves_by_board
をbestmoves_and_score_by_board
に修正する
1 from copy import deepcopy
2
3 def calc_children(self, bestmoves_and_score_by_board=None):
4 self.children = []
5 for x, y in self.mb.calc_legal_moves():
6 childmb = deepcopy(self.mb)
7 childmb.move(x, y)
8 self.insert(Node(childmb, parent=self, depth=self.depth + 1,
9 bestmoves_and_score_by_board=bestmoves_and_score_by_board))
10
11 Node.calc_children = calc_children
行番号のないプログラム
from copy import deepcopy
def calc_children(self, bestmoves_and_score_by_board=None):
self.children = []
for x, y in self.mb.calc_legal_moves():
childmb = deepcopy(self.mb)
childmb.move(x, y)
self.insert(Node(childmb, parent=self, depth=self.depth + 1,
bestmoves_and_score_by_board=bestmoves_and_score_by_board))
Node.calc_children = calc_children
修正箇所
from copy import deepcopy
-def calc_children(self, bestmoves_by_board=None):
+def calc_children(self, bestmoves_and_score_by_board=None):
self.children = []
for x, y in self.mb.calc_legal_moves():
childmb = deepcopy(self.mb)
childmb.move(x, y)
self.insert(Node(childmb, parent=self, depth=self.depth + 1,
- bestmoves_by_board=bestmoves_by_board))
+ bestmoves_and_score_by_board=bestmoves_and_score_by_board))
Node.calc_children = calc_children
Mbtree クラスの create_subtree
の修正
次に、Mbtree クラスの create_subtree
の中で ノードを作成する処理を修正 します。
局面と最善手・評価値の対応表 のデータは、create_subtree
の仮引数 subtree
に代入する dict の bestmoves_and_score_by_board
というキーの値に代入することにし、その値を使って下記のプログラムのようにノードを作成するように修正します。
-
4 行目:
bestmoves_and_score_by_board
に局面と最善手・評価値の対応表のデータを代入するように修正する -
5、7、8、9、11 行目:
bestmoves_by_board
をbestmoves_and_score_by_board
に修正する -
9 行目:元の
bestmoves_by_board[board_str]
には最善手の一覧を表す list が代入されていたが、修正後のbestmoves_and_score_by_board[board_str]
には最善手の一覧と評価値を表す dict が代入されているので、bestmoves_and_score_by_board[board_str]["bestmoves"]
のように修正する必要がある点に注意すること
1 from marubatsu import Marubatsu
2
3 def create_subtree(self):
4 bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
5 self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略
6 childnode = Node(childmb, parent=node, depth=depth+1,
7 bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略
8 node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略
9 x, y = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
元と同じなので省略
10 childnode = Node(childmb, parent=node, depth=depth+1,
11 bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略
12
13 Mbtree.create_subtree = create_subtree
行番号のないプログラム
from marubatsu import Marubatsu
def create_subtree(self):
bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
depth = 0
nodelist = [self.root]
centermb = self.subtree["centermb"]
centerdepth = centermb.move_count
records = centermb.records
maxdepth = self.subtree["maxdepth"]
while len(nodelist) > 0:
childnodelist = []
for node in nodelist:
if depth < centerdepth - 1:
childmb = deepcopy(node.mb)
x, y = records[depth + 1]
childmb.move(x, y)
childnode = Node(childmb, parent=node, depth=depth+1,
bestmoves_and_score_by_board=bestmoves_and_score_by_board)
node.insert(childnode)
childnodelist.append(childnode)
elif depth < maxdepth:
node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)
if depth == centerdepth - 1:
for move, childnode in node.children_by_move.items():
if move == records[depth + 1]:
self.centernode = childnode
childnodelist.append(self.centernode)
else:
if childnode.mb.status == Marubatsu.PLAYING:
childnode.children.append(None)
else:
childnodelist += node.children
else:
if node.mb.status == Marubatsu.PLAYING:
childmb = deepcopy(node.mb)
board_str = node.mb.board_to_str()
x, y = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
childmb.move(x, y)
childnode = Node(childmb, parent=node, depth=depth+1,
bestmoves_and_score_by_board=bestmoves_and_score_by_board)
node.insert(childnode)
childnodelist.append(childnode)
nodelist = childnodelist
depth += 1
selectedmb = self.subtree["selectedmb"]
self.selectednode = self.root
for move in selectedmb.records[1:]:
self.selectednode = self.selectednode.children_by_move[move]
Mbtree.create_subtree = create_subtree
修正箇所
from marubatsu import Marubatsu
def create_subtree(self):
- bestmoves_by_board = self.subtree["bestmoves_by_board"]
+ bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
- self.root = Node(Marubatsu(), bestmoves_by_board=bestmoves_by_board)
+ self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略
childnode = Node(childmb, parent=node, depth=depth+1,
- bestmoves_by_board=bestmoves_by_board)
+ bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略
- node.calc_children(bestmoves_by_board=bestmoves_by_board)
+ node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略
- x, y = bestmoves_by_board[board_str][0]
+ x, y = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
元と同じなので省略
childnode = Node(childmb, parent=node, depth=depth+1,
- bestmoves_by_board=bestmoves_by_board)
+ bestmoves_and_score_by_board=bestmoves_and_score_by_board)
元と同じなので省略
Mbtree.create_subtree = create_subtree
上記の修正後に、下記のプログラムを実行して前回の記事と同様の下記のような部分木を作成して表示すると、実行結果のように評価値が表示されるようになったことが確認できます。ただし、一番上の評価値の上部が灰色の背景の部分からはみ出して表示される という問題が生じます。この問題の原因と修正する方法について少し考えてみて下さい。
- 中心となるノードは (0, 0) と (1, 0) に着手を行った局面のノード
- 選択されたノードは、さらに (2, 0) に着手を行った局面のノード
- 中心となるノードから深さ 3 までの全ての子孫ノードを持つ部分木を計算し、それ以降の深さでは最善手を着手し続けた局面のノードを計算する
mb = Marubatsu()
mb.move(0, 0)
mb.move(1, 0)
mb2 = Marubatsu()
mb2.move(0, 0)
mb2.move(1, 0)
mb2.move(2, 0)
maxdepth = 3
subtree = Mbtree(subtree={"centermb": mb, "selectedmb": mb2, "maxdepth": maxdepth,
"bestmoves_and_score_by_board": bestmoves_and_score_by_board})
centernode = subtree.centernode
selectednode = subtree.selectednode
subtree.draw_subtree(centernode=centernode, selectednode=selectednode, maxdepth=maxdepth,
show_bestmove=True)
修正箇所
mb = Marubatsu()
mb.move(0, 0)
mb.move(1, 0)
mb2 = Marubatsu()
mb2.move(0, 0)
mb2.move(1, 0)
mb2.move(2, 0)
maxdepth = 3
subtree = Mbtree(subtree={"centermb": mb, "selectedmb": mb2, "maxdepth": maxdepth,
- "bestmoves_by_board": bestmoves_by_board})
+ "bestmoves_and_score_by_board": bestmoves_and_score_by_board})
centernode = subtree.centernode
selectednode = subtree.selectednode
subtree.draw_subtree(centernode=centernode, selectednode=selectednode, maxdepth=maxdepth,
show_bestmove=True)
実行結果
問題の検証と修正
一番上の評価値が灰色の背景からはみ出るというバグは、以前の記事で Mbtree_GUI クラスを使って部分木を表示 した際にも発生しました。その際には Mbtree_GUI クラスで作成する、部分木を表示する Figure の大きさ と Axes の表示範囲を修正 することでバグの修正を行いました。一方、先程のプログラムは Mbtree_GUI クラスからではなく、直接 draw_subtree
メソッドを呼び出して部分木を表示 しているので、以前の記事の Mbtree_GUI クラスの修正は反映されません。
先程のプログラムのように、キーワード引数 ax
を記述せず に draw_subtree
で部分木を表示 した場合は、draw_subtree
の中で Figure を作成し、Axes の表示範囲を設定しているので、その部分を一番上の評価値が灰色の背景からはみでないように修正する必要があります。下記は、そのように draw_subtree
を修正したプログラムです。
- 7 行目:一番上の評価値がはみでないようするために、Figrue の高さを 1 増やす
- 9 行目:Axes の y 座標の表示範囲の最小値を 0 から -1 に修正する
1 import matplotlib.pyplot as plt
2 import matplotlib.patches as patches
3
4 def draw_subtree(self, centernode=None, selectednode=None, ax=None, anim_frame=None,
5 isscore=False, show_bestmove=False, show_score=True, size=0.25, lw=0.8, maxdepth=2):
元と同じなので省略
6 if ax is None:
7 fig, ax = plt.subplots(figsize=(width * size, (height + 1) * size))
8 ax.set_xlim(0, width)
9 ax.set_ylim(-1, height)
10 ax.invert_yaxis()
11 ax.axis("off")
元と同じなので省略
12
13 Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
import matplotlib.pyplot as plt
import matplotlib.patches as patches
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)
if show_bestmove:
width = 5 * 10
else:
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 + 1) * size))
ax.set_xlim(0, width)
ax.set_ylim(-1, 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
emphasize = bestnode is selectednode
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
修正箇所
import matplotlib.pyplot as plt
import matplotlib.patches as patches
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):
元と同じなので省略
if ax is None:
- fig, ax = plt.subplots(figsize=(width * size, height * size))
+ fig, ax = plt.subplots(figsize=(width * size, (height + 1) * size))
ax.set_xlim(0, width)
- ax.set_ylim(0, height)
+ ax.set_ylim(-1, height)
ax.invert_yaxis()
ax.axis("off")
元と同じなので省略
Mbtree.draw_subtree = draw_subtree
上記の修正後に下記のプログラムを実行すると、実行結果のように一番上の評価値が灰色の背景からはみでないようになります。
subtree = Mbtree(subtree={"centermb": mb, "selectedmb": mb2, "maxdepth": maxdepth,
"bestmoves_and_score_by_board": bestmoves_and_score_by_board})
centernode = subtree.centernode
selectednode = subtree.selectednode
subtree.draw_subtree(centernode=centernode, selectednode=selectednode, maxdepth=maxdepth,
show_bestmove=True)
実行結果
Mbtree_GUI クラスの修正
次に create_subtree
によって 動的に作成した部分木を描画 するように Mbtree_GUI クラスを修正します。どのように修正すれば良いかについて少し考えてみて下さい。
__init__
メソッドの修正
下記は、現状の Mbtree_GUI クラスの __init__
メソッドの定義です。
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__()
Mbtree_GUI クラスは、これまではゲーム木全体を表す Mbtree クラスのインスタンスを使って部分木の描画を行っていましたが、そのデータは もう必要がない ので、そのデータを代入する 仮引数 mbtree
を __init__
メソッドから削除する 必要があります。また、その代わりに 部分木動的に作成するために必要 となる 局面と最善手・評価値の対応表 のデータを代入する 仮引数を __init__
メソッドに追加 する必要があります。
また、上記の 7 行目では 選択されたノード を表す selectednode
属性を、ゲーム木の ルートノードのデータで初期化 していますが、仮引数 mbtree
を削除 してしまうと、この時点では ゲーム木のデータは存在しない ので 7 行目を実行すると エラーが発生 してしまいます。7 行目で selectednode
属性に何を代入すればよいかについては、現時点では判断できないので保留 し、後で 7 行目の処理を修正することにします。
下記は __init__
メソッドを修正したプログラムです。
-
3 行目:仮引数
mbtree
を削除し、代わりに部分木を動的な作成に必要となる、局面と最善手・評価値の対応表のデータを代入する仮引数bestmoves_and_score_by_board
を追加する -
4 行目:
mbtree
属性に同名の仮引数を代入する処理を削除し、代わりにbestmoves_and_score_by_board
属性に同名の仮引数を代入する処理を追加する -
9 行目:このままでは
self.mbtree
に値が代入されていないのでエラーが発生するが、この部分の修正は後回しにする
1 from tree import Mbtree_GUI
2
3 def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):
4 self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
5 self.show_score = show_score
6 self.size = size
7 self.width = 50
8 self.height = 65
9 self.selectednode = self.mbtree.root
10 super(Mbtree_GUI, self).__init__()
11
12 Mbtree_GUI.__init__ = __init__
行番号のないプログラム
from tree import Mbtree_GUI
def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):
self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
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__
修正箇所
from tree import Mbtree_GUI
-def __init__(self, mbtree, show_score=True, size=0.15):
+def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):
- self.mbtree = mbtree
+ self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
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__
update_gui
メソッドの修正
部分木の表示 の処理を行う update_gui
メソッドは、これまでは下記のプログラムによって、下記の手順で部分木を表示していました。
-
2 ~ 7 行目:選択されたノードを表す
self.selectednode
の深さ(depth
属性)から、maxdepth
を計算 する -
8 ~ 10 行目:
self.selectednode
から 中心となるノード を下記の手順で 計算する- 深さが 6 以下の場合は、選択されたノードを中心となるノードとする
- 深さが 6 より大きい場合は、親ノード(
parent
属性)をさかのぼって、深さが 6 の先祖ノードを中心となるノードとする
-
11 ~ 13 行目:ゲーム木全体のデータが代入された
self.mbtree
のdraw_subtree
メソッドを呼び出して部分木を描画する
1 def update_gui(略):
略
2 if self.selectednode.depth <= 4:
3 maxdepth = self.selectednode.depth + 1
4 elif self.selectednode.depth == 5:
5 maxdepth = 7
6 else:
7 maxdepth = 9
8 centernode = self.selectednode
9 while centernode.depth > 6:
10 centernode = centernode.parent
11 self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
12 show_bestmove=True, show_score=self.show_score,
13 ax=self.ax, maxdepth=maxdepth, size=self.size)
略
上記の処理を、部分木を動的に作成 し、その部分木を表示する ように修正する必要があります。部分木を動的に作成する create_subtree
メソッドは、選択されたノードの局面 を表すデータ、中心となるノードの局面 を表すデータ、すべての子孫ノードを計算するノードの 最大の深さを表す maxdepth
を必要とし、それらのデータは上記のプログラムの 2 ~ 10 行目の処理を行った結果、下記の変数に代入 されます。
データ | 代入される変数 |
---|---|
選択されたノードの局面を表すデータ | self.selectednode.mb |
中心となるノードの局面を表すデータ | centernode.mb |
最大の深さ | maxdepth |
従って、上記のデータを利用 する事で 部分木を動的に作成 して 描画 を行えます。
下記は、そのように update_gui
を修正したプログラムです。
- 2 ~ 10 行目:この部分のプログラムは修正する必要はない
-
11、12 行目:2 ~ 10 行目で計算したデータを利用して部分木を作成し、Mbtree_GUI クラスが表示するゲーム木のデータを表す
mbtree
属性に代入 する -
13 行目:
self.mbtree
に代入した部分木は、新しく作成 したものなので、その中の 選択されたノードを表すself.mbtree.selectednode
は、self.selectednode
に代入されているノード とは別のノード である。そのため、self.selectednode
に新しく作成された部分木の選択されたノードを代入して 更新する必要 がある -
14 行目:同様に、中心となるノードも
centernode
に代入されているノードとは別のノードなのでcenternode
を 新しく作成した部分木の中心となるノードに更新 する
1 def update_gui(self):
元と同じなので省略
2 if self.selectednode.depth <= 4:
3 maxdepth = self.selectednode.depth + 1
4 elif self.selectednode.depth == 5:
5 maxdepth = 7
6 else:
7 maxdepth = 9
8 centernode = self.selectednode
9 while centernode.depth > 6:
10 centernode = centernode.parent
11 self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
12 "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
13 self.selectednode = self.mbtree.selectednode
14 centernode = self.mbtree.centernode
15 self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
16 show_bestmove=True, show_score=self.show_score,
17 ax=self.ax, maxdepth=maxdepth, size=self.size)
元と同じなので省略
18
19 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 = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
"bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
self.selectednode = self.mbtree.selectednode
centernode = self.mbtree.centernode
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):
元と同じなので省略
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 = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
+ "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
+ self.selectednode = self.mbtree.selectednode
+ centernode = self.mbtree.centernode
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)
元と同じなので省略
Mbtree_GUI.update_gui = update_gui
上記の 2 ~ 14 行目で行った修正によって、self.mbtree
、self.selectednode
、centernode
に 新しく作成した部分木に対応するデータが代入 されるため、それらの値を使って 部分木の表示を行う 15 行目 のプログラムを 変更する必要はありません。
また、2 ~ 14 行目では それら以外の変数や属性の値を変更する処理は行っていない ので、Mbtree_GUI クラスの 他のプログラムを修正する必要はありません。例えば、create_event_handler
メソッドに記述されている処理を変更する必要はありません。
__init__
メソッド内で selectenode
属性に代入する値
上記で他のプログラムを修正する必要はないと説明しましたが、それは update_gui
の 2 ~ 14 行目の処理が 行われた後 で実行される処理を修正する必要はないという意味です。
先程保留した __init__
メソッドで selectednode
属性の初期化 を行う処理は、update_gui
の 2 ~ 14 行目の処理を行う前に実行される ため、update_gui
の 2 ~ 14 行目の処理を 正しく行うことができるようなデータで初期化する 必要があります。
update_gui
の 2 ~ 14 行目で、self.selectednode
に対して行われる処理 は、selectednode
の深さを表す depth
属性 と、親ノードを表す parent
属性 に対する処理だけです。従って、それらの属性に適切なデータが代入 されている ルートノードのデータ で self.selectednode
を 初期化すればよい ことがわかります。
従って、__init__
メソッドの中で self.selectednode
に代入する ルートノードを表すデータ は、下記の理由から Node(Marubatsu())
を代入すればよいことがわかります。
- ゲーム開始時の局面のデータは
Marubatsu()
によって作成できる - ルートノードには親ノードは存在しないので、キーワード引数
parent
を記述する必要はない - ルートノードの深さは 0 で、Node クラスの
__init__
メソッドの深さを代入する仮引数depth
はデフォルト値が 0 として定義されているので、キーワード引数depth
を記述する必要はない - 最善手の一覧や評価値のデータを計算する必要はないので、キーワード引数
bestmoves_and_score_by_board
を記述する必要はない - 子ノードのデータは必要がないので、ルートノードを作成した後で、子ノードのデータを計算する必要はない
下記はそのように __init__
メソッドを修正したプログラムです。
-
8 行目:ゲーム開始時の局面を表すノードを作成 して
selectednode
属性に代入 する
1 def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):
2 self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
3 self.show_score = show_score
4 self.size = size
5 self.width = 50
6 self.height = 65
7 self.selectednode = Node(Marubatsu())
8 super(Mbtree_GUI, self).__init__()
9 Mbtree_GUI.__init__ = __init__
行番号のないプログラム
def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):
self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
self.show_score = show_score
self.size = size
self.width = 50
self.height = 65
self.selectednode = Node(Marubatsu())
super(Mbtree_GUI, self).__init__()
Mbtree_GUI.__init__ = __init__
修正箇所
def __init__(self, bestmoves_and_score_by_board, show_score=True, size=0.15):
self.bestmoves_and_score_by_board = bestmoves_and_score_by_board
self.show_score = show_score
self.size = size
self.width = 50
self.height = 65
- self.selectednode = self.mbtree.root
+ self.selectednode = Node(Marubatsu())
super(Mbtree_GUI, self).__init__()
Mbtree_GUI.__init__ = __init__
上記の修正後に下記のプログラムを実行すると、実行結果のようにエラーが発生します。このエラーの原因について少し考えてみて下さい。
Mbtree_GUI(bestmoves_and_score_by_board)
実行結果
略
Cell In[11], line 20
17 self.mbtree = Mbtree(subtree={"centermb": centernode.mb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
18 "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
19 self.selectednode = self.mbtree.selectednode
---> 20 centernode = self.mbtree.centernode
21 self.mbtree.draw_subtree(centernode=centernode, selectednode=self.selectednode,
22 show_bestmove=True, show_score=self.show_score,
23 ax=self.ax, maxdepth=maxdepth, size=self.size)
25 disabled = self.selectednode.parent is None
AttributeError: 'Mbtree' object has no attribute 'centernode'
エラーの原因の検証と修正
エラーメッセージから centernode=self.mbtree.centernode
を実行した際に、self.mbtree
に centernode
属性が存在しない ことがわかります。そこで、create_subtree
内で centernode
属性を計算する処理を検証 ことにします。
下記は create_subtree
の centernode
に値を代入する処理に関連するプログラムです。
1 def create_subtree(self):
2 bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
3 self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
4
5 depth = 0
6 nodelist = [self.root]
略
7 while len(nodelist) > 0:
略
8 node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)
9 if depth == centerdepth - 1:
10 for move, childnode in node.children_by_move.items():
11 if move == records[depth + 1]:
12 self.centernode = childnode
略
上記から、centernode
に値を代入する処理は、7 行目の while
文による繰り返しの処理の中で、以下の手順で行われることがわかります。
- 8 行目で
node
の子ノードを作成する - 11 行目で
node
の子ノードが中心となるノードと判定された場合に、12 行目でcenternode
属性にその子ノードを代入する
一方、部分木の ルートノードは 3 行目で作成 されますが、ルートノード は他のノードの 子ノードではありません。そのため、ルートノードが中心となるノードの場合 は、11 行目の処理が実行されることはないため、centernode
属性に値は代入されません。
Mbtree_GUI クラスの __init__
メソッドでは、ルートノードを selectednode
に代入 し、その場合は update_gui
内の処理で ルートノードが中心となるノードとなる ので centernode
属性は計算されず、先程のようなエラーが発生することになります。
従って、このエラーは下記のプログラムのように、中心となるノードがルートノードの場合 に centernode
属性にルートノードを代入する ことで修正することができます。
-
9、10 行目:深さが 0 のノードはルートノードのみなので、中心となるノードの深さが 0 の場合に
centernode
属性にルートノードを代入する
1 def create_subtree(self):
元と同じなので省略
2 bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
3 self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
4
5 depth = 0
6 nodelist = [self.root]
7 centermb = self.subtree["centermb"]
8 centerdepth = centermb.move_count
9 if centerdepth == 0:
10 self.centernode = self.root
元と同じなので省略
11
12 Mbtree.create_subtree = create_subtree
行番号のないプログラム
def create_subtree(self):
bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
depth = 0
nodelist = [self.root]
centermb = self.subtree["centermb"]
centerdepth = centermb.move_count
if centerdepth == 0:
self.centernode = self.root
records = centermb.records
maxdepth = self.subtree["maxdepth"]
while len(nodelist) > 0:
childnodelist = []
for node in nodelist:
if depth < centerdepth - 1:
childmb = deepcopy(node.mb)
x, y = records[depth + 1]
childmb.move(x, y)
childnode = Node(childmb, parent=node, depth=depth+1,
bestmoves_and_score_by_board=bestmoves_and_score_by_board)
node.insert(childnode)
childnodelist.append(childnode)
elif depth < maxdepth:
node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)
if depth == centerdepth - 1:
for move, childnode in node.children_by_move.items():
if move == records[depth + 1]:
self.centernode = childnode
childnodelist.append(self.centernode)
else:
if childnode.mb.status == Marubatsu.PLAYING:
childnode.children.append(None)
else:
childnodelist += node.children
else:
if node.mb.status == Marubatsu.PLAYING:
childmb = deepcopy(node.mb)
board_str = node.mb.board_to_str()
x, y = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
childmb.move(x, y)
childnode = Node(childmb, parent=node, depth=depth+1,
bestmoves_and_score_by_board=bestmoves_and_score_by_board)
node.insert(childnode)
childnodelist.append(childnode)
nodelist = childnodelist
depth += 1
selectedmb = self.subtree["selectedmb"]
self.selectednode = self.root
for move in selectedmb.records[1:]:
self.selectednode = self.selectednode.children_by_move[move]
Mbtree.create_subtree = create_subtree
修正箇所
def create_subtree(self):
元と同じなので省略
bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
depth = 0
nodelist = [self.root]
centermb = self.subtree["centermb"]
centerdepth = centermb.move_count
if centerdepth == 0:
self.centernode = self.root
元と同じなので省略
Mbtree.create_subtree = create_subtree
実行結果は省略しますが、上記の修正後に下記のプログラムを実行するとルートノードが選択された部分木が表示され、上部のボタンをクリックしたり、ノードをクリックするとこれまでと同様に選択されたノードが正しく変更されます。実際に確認してみて下さい。
Mbtree_GUI(bestmoves_and_score_by_board)
また、下記のプログラムのように、最短の勝利を優先した評価値を計算したデータ で Mbtree_GUI クラスのインスタンスを作成すると、実行結果のように部分木にそのような評価値が表示されるようになることが確認できます。なお、下図では 2.0 という評価値が表示されるように、(1, 0) に着手を行った局面を選択状態にしました。
Mbtree_GUI(bestmoves_and_score_by_board_shortest_victory)
実行結果
本記事のまとめ
本記事では、局面と最善手・評価値の対応表のデータを作成してファイルに保存するプログラムを作成し、そのデータ利用して部分木を表示するように Mbtree_GUI を修正しました。また、それによって異なる方法で評価値を計算したゲーム木の部分木を表示することができることを示しました。
次回の記事では Mbtree_GUI クラスで表示するゲーム木の種類を後から変更できるようにし、gui_play
で部分木を正しく表示できるように修正する予定です。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
tree_new.py | 今回の記事で更新した tree.py |
util_new.py | 今回の記事で更新した util_new.py |
bestmoves_and_score_by_board.dat | 今回の記事で作成した、局面と最善手・評価値の対応表のデータを保存したファイル |
bestmoves_and_score_by_board_shortest_victory.dat | 今回の記事で作成した、最短の勝利を優先した評価値を計算した局面と最善手・評価値の対応表のデータを保存したファイル |
次回の記事