目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
gui_play
の実行速度についての補足
前回の記事で説明し忘れていた点を最初に補足します。前回の記事でMarubatsu_GUI クラスのインスタンスを作成する際に、ゲーム木のデータをファイルから読み込まなくなりました。そのため、gui_play
メソッドを実行する際に待たされることが無くなっています。下記のプログラムを実行してそのことを確認して下さい。
from util import gui_play
gui_play()
リプレイ機能を利用した際のバグの検証と修正
前回の記事では、部分木を表示する際に利用する、局面と最善手・評価値の対応表を Dropdown で変更できるように Mbtree_GUI クラスを修正しましたが、その結果 バグが発生 しています。そのバグは、リプレイ機能を利用した際に発生 するバグで、例えば下記の手順で操作を行うと発生します。
-
gui_play()
を実行し、(0, 0)、(1, 0) の順で着手を行う - 1 手前の局面を表示 する < ボタン1をクリックする と、下図のように ゲーム盤の表示 と、GUI の部分木の赤枠の 選択されたノードの局面 が 一致しなくなる
- さらに < ボタンをクリックすると下記のような エラーが発生 する
略
File c:\Users\ys\ai\marubatsu\124\tree.py:507, in Mbtree.create_subtree(self)
505 self.selectednode = self.root
506 for move in selectedmb.records[1:]:
--> 507 self.selectednode = self.selectednode.children_by_move[move]
KeyError: (1, 0)
上記の 2 種類のバグが発生する原因について少し考えてみて下さい。
バグの原因の検証
実は、このバグを 修正するのは簡単 ですが、このバグの 原因を理解して説明する のはそれほど 簡単ではない のでバグの原因の見つけ方を説明します。
バグは 2 つありますが、1 つ目のバグはエラーが発生しないバグなので、先にエラーが発生する 2 つ目のバグの原因について検証することにします。
エラーメッセージの検証
上記のエラーメッセージから、エラーが Mbtree クラスの create_subtree
内で発生 したことがわかります。また、エラーの原因が self.selectednode.children_by_move
に代入された dict に (1, 0)
というキーが存在しない 可能性が高いことがわかります。
エラーメッセージの付近で行われる下記のプログラムの処理は、create_subtree
で作成した 部分木の中から、selectedmb
を使って 選択されたノードを探す というものです。
self.selectednode = self.root
for move in selectedmb.records[1:]:
self.selectednode = self.selectednode.children_by_move[move]
上記のプログラムでは、create_subtree
が作成した部分木の ルートノードを表す self.root
と、選択された局面の 棋譜を表す selectedmb.records
属性の値を利用して処理を行っているので、それらの値がどのように計算されるかを検証 することにします。
< ボタンをクリックした時に行われる処理の検証
バグは < ボタンをクリックした時に発生する ので、< ボタンをクリックした時に、上記の処理が行われるまでの処理を検証することにします。
on_prev_button_clicked
の処理
< ボタンをクリックすると、Marubatsu_GUI クラスの create_event_handler
メソッド内で定義された on_prev_button_clicked
が呼び出され、以下の手順で処理が行われます。
- 9 行目の処理によって、実引数に 一手前の手数 を記述して 3 行目で定義された
change_step
を呼び出す - 4 行目で実引数に 一手前の手数 を記述して、Marubatsu クラスの
change_step
メソッドを呼び出すことで、self.mb
を一手前の局面にする - 6 行目で GUI の部分木の描画を更新する
1 def create_event_handler(self):
略
2 # step 手目の局面に移動する
3 def change_step(step):
4 self.mb.change_step(step)
5 # 描画を更新する
6 self.update_gui()
7
8 def on_prev_button_clicked(b=None):
9 change_step(self.mb.move_count - 1)
略
Marubatsu クラスの change_step
の処理
上記の 4 行目で呼び出される Marubatsu クラスの change_step
は下記のプログラムのように定義されており、records
属性に記録された 棋譜に従って、ゲーム開始時の局面から step
回の着手を行う ことで step
手目の局面にする 処理を行います。
change_step
について忘れた方は、以前の記事を復習して下さい。
1 def change_step(self, step):
2 # step の範囲を正しい範囲に修正する
3 step = max(0, min(len(self.records) - 1, step))
4 records = self.records
5 self.restart()
6 for x, y in records[1:step+1]:
7 self.move(x, y)
8 self.records = records
リプレイ機能 では change_step
を使って 任意の手数の局面を計算 していますが、その際に上記の棋譜のデータが記録された records
属性を利用 しています。そのため、change_step
を実行した際に records
属性の値が変化しないようにする 必要があり、その処理を上記の 4 行目と 8 行目 で行っています。
リプレイ機能 で局面を移動しても、棋譜を表す records
属性の値が変化しない ことを覚えておいてください。このことが、バグの原因になっているからです。
Marubatsu_GUI クラスの update_gui
の処理
次に、ゲーム盤の表示を更新 するために Marubatsu_GUI クラスの update_gui
メソッドが呼び出されます。update_gui
メソッドでは self.mb
の ゲーム盤の表示の更新 を行いますが、先ほど行った処理によって self.mb
は一手前の局面 になっているので 一手前の局面が表示 されます。そのことは、< ボタンをクリックすることで実際に一手前の局面のゲーム盤が表示されることからも確認できます。
その後で、下記の 3 ~ 7 行目のプログラムで GUI の部分木の表示の更新 の処理を行います。、下記のプログラムの 5 行目で 一手前の局面を GUI の部分木の 選択されたノード とし、6 行目で GUI の部分木の表示を更新 する処理を行います。
1 def update_gui(self):
略(ゲーム盤の表示の更新の処理)
2
3 if hasattr(self, "mbtree_gui"):
4 from tree import Node
5
6 self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
7 self.mbtree_gui.update_gui()
Mbtree_GUI クラスの update_gui
の処理
上記の 7 行目で呼び出された Mbtree_GUI クラスの update_gui
メソッドでは、下記の手順で 部分木の作成の処理 が行われます。
-
2 ~ 7 行目:計算する子孫ノードの深さの最大値を表す
maxdepth
を計算する -
8 ~ 13 行目:中心となるノードの局面を表す
centermb
を計算する - 14、15 行目:上記で計算したデータを使って、部分木を作成する処理を呼び出す
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 if self.selectednode.depth <= 6:
9 centermb = self.selectednode.mb
10 else:
11 centermb = Marubatsu()
12 for x, y in self.selectednode.mb.records[1:7]:
13 centermb.move(x, y)
14 self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
15 "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
略
selectednode
に代入された 一手前の局面のノード は、(0, 0) の着手が行われた 1 手目の局面 なので、上記の処理によって maxdepth
には 2
が、centermb
には選択されたノードと同じ局面 が代入されて、部分木が作成されます。
下図は < ボタンを一回クリック した際に表示される GUI の部分木 で、確かに (0, 0) の着手が行われた局面を中心とする部分木が作成 されていることが確認できます。
上記の表示で 間違っている のは 赤枠の選択されたノード なので、部分木を作成する処理の中で 選択されたノードを計算する処理を探す と、create_subtree
の下記のプログラムでその処理が行われていることがわかります。この処理は、< を 2 回クリックした際にエラーが発生 したプログラムなので、1 つ目のバグの原因もこの処理である可能性が高い ことがわかりました。
1 def create_subtree(略):
略
2 self.selectednode = self.root
3 for move in selectedmb.records[1:]:
4 self.selectednode = self.selectednode.children_by_move[move]
create_subtree
の処理
バグの原因の可能性が高いプログラムを絞り込む ことができたので、上記のプログラムで行われる処理を検証することにします。
selectedmb
は、(0, 0)、(1, 0) の 2 回の着手を行った後 で、Marubatsu クラスの change_step
メソッドによって (0, 0) の着手が行われた 1 手目の局面を計算 したものです。先程説明したように、change_step
を実行しても、その棋譜を表す records
属性の値は変化しない ので、selectedmb.records
には (0, 0) と (1, 0) の 2 手分の着手が記録 されています。上記の 3 行目の for 文では、records
属性に記録された すべての着手に対する繰り返し処理 が行われるので、selectednode
には、(0, 0) と (1, 0) の 2 手分の着手を行った局面に対するノードが計算 されて代入されることになります。
これが < ボタンを 1 回クリックした際に、上部のゲーム盤には一手前の局面が表示されるが、下部の GUI の部分木の赤枠の 選択された局面が変化しない ことの原因です。
バグの修正
従って、1 つ目のバグは、下記のプログラムの 7 行目のように、棋譜に記録された着手 を、現在の手数を表す move_count
属性の数だけ行う ようにすることで修正することができます2。また、実は 2 つ目のエラーが発生するバグの原因 は、1 つ目のバグと同じ なので、この修正によって 両方のバグを修正 することができます。
1 from tree import Mbtree, Node
2 from marubatsu import Marubatsu
3 from copy import deepcopy
4
5 def create_subtree(self):
元と同じなので省略
6 self.selectednode = self.root
7 for move in selectedmb.records[1:selectedmb.move_count+1]:
8 self.selectednode = self.selectednode.children_by_move[move]
9
10 Mbtree.create_subtree = create_subtree
行番号のないプログラム
from tree import Mbtree, Node
from marubatsu import Marubatsu
from copy import deepcopy
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:selectedmb.move_count+1]:
self.selectednode = self.selectednode.children_by_move[move]
Mbtree.create_subtree = create_subtree
修正箇所
from tree import Mbtree, Node
from marubatsu import Marubatsu
from copy import deepcopy
def create_subtree(self):
元と同じなので省略
self.selectednode = self.root
- for move in selectedmb.records[1:]:
+ for move in selectedmb.records[1:selectedmb.move_count+1]:
self.selectednode = self.selectednode.children_by_move[move]
Mbtree.create_subtree = create_subtree
上記の修正後に下記のプログラムを実行し、先程と同じ手順で操作を行ってもバグが発生しなくなったことを確認して下さい。また、様々な着手とリプレイ機能のボタンの操作を行い、プログラムが正しく動作することを確認して下さい。
gui_play()
2 つ目のバグでエラーが発生する原因
1 つ目のバグ では エラーが発生しない が、2 つ目のバグ で エラーが発生する 点が気になっている人がいるかもしれません。その理由について少し考えてみて下さい。
2 つ目のバグは、(0, 0)、(1, 0) の着手を行った後で、< ボタンを 2 回クリック しているので、ゲーム開始時 の 深さ 0 のノード が GUI の部分木の 選択されたノード になります。
その場合は、Mbtree_GUI クラスの update_gui
の下記の処理によって、maxdepth
には 1
が、centermb
にはゲーム開始時の局面 が代入されて部分木が計算されます。
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 if self.selectednode.depth <= 6:
9 centermb = self.selectednode.mb
10 else:
11 centermb = Marubatsu()
12 for x, y in self.selectednode.mb.records[1:7]:
13 centermb.move(x, y)
14 self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth,
15 "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
略
その結果、作成される部分木は、下図のように 深さ 1 までの子孫ノードはすべて計算 されますが、深さ 2 以降の子孫ノード は 最善手を計算し続けた場合の局面のノードしか計算されません。また、下図からわかるように (0, 0) に着手を行ったノードの子ノード には、(0, 0)、(1, 1) に着手 を行ったノード しか存在しません。
そのため、修正前の下記のプログラムを実行すると、ルートノードから (0, 0)、(1, 0) の順で着手を行ったノードを計算しようとした結果、(0, 0) に着手を行ったノード の children_by_move
属性に代入された dict には (0, 1) というキーは存在しない ので、KeyError: (1, 0) というエラーが発生することになります。
1 def create_subtree(略):
略
2 self.selectednode = self.root
3 for move in selectedmb.records[1:]:
4 self.selectednode = self.selectednode.children_by_move[move]
このことから、バグを修正する前に、(0, 0)、(1, 1) の順で着手 を行い、< ボタンを 2 回クリックした場合は、作成した部分木に (0, 0)、(1, 1) の着手を行ったノードが 偶然存在する ので エラーは発生しない ことがわかります。興味がある方は、バグを修正する前のプログラムに戻して実際に試してみて下さい。
また、(0, 0)、(1, 1) の順で着手 を行った後で < ボタンを 1 回クリック した際に作成される部分木は、下図のように 深さ 2 までの子孫ノードがすべて計算される ので、(0, 0) に着手を行ったノードの子ノードには (1, 0) に着手を行ったノードが存在します。そのため 2 手目までの着手 を行った局面で < ボタンを 1 回クリック した場合は、2 手目までにどのような着手を行っても エラーは発生しません。
このような、バグがあるにも関わらす特定の条件ではエラーが発生しない バグは、< ボタンを 1 回だけクリックした場合のように、偶然その条件を満たしてしまう とバグの存在が みづけづらい ので 非常に厄介なバグ です。このようなバグは、今回の記事のように実際によく発生することがあるので、詳しく紹介しました。
Marubatsu_GUI クラスの改良
Marubatsu_GUI クラスの 改良 をいくつか思いついたので実装することにします。
行う改良は以下の通りです。最初の 3 つは同様の処理を別の所で実装済なので、過去で行った実装を参考に実装することにします。
4 つ目以降の改良については次回の記事で行います。
- FloatSlider でゲーム盤の表示の大きさを変更できるようにする
- GUI の部分木の表示の有無を切り替える「木」ボタンを、「評価値の表示」ボタンのように ON/OFF の状態がわかるようにする
- GUI の部分木の選択されたノードを、ゲーム盤に表示されている局面のノードにリセットする「リ」ボタンを、GUI の部分木の表示が OFF になっている場合は操作できないようにする
- 現在の局面の状況がわかるようにする
- ゲーム盤のマスに、そのマスに着手を行った場合の局面の状況を表示する
- ゲーム盤のマスに、そのマスに着手を行った際の AI の評価値を表示できるようにする
最初の 3 つの改良はこれまでに同様の処理を行っているのでまとめて行うことにします。
Marubatsu_GUI クラス の __init__
メソッドの修正
Mbtree_GUI
クラスでは、部分木に評価値を表示するかどうかを show_score
という属性に代入しましたので、Marubatsu_GUI クラスでも GUI の部分木を表示するか どうかを show_subtree
という属性に代入することにします。
まず、最初に GUI の部分木を表示するか どうかを Marubatsu_GUI クラスの __init__
メソッドの仮引数 show_subtree
に代入することにします。
下記は、そのように __init__
メソッドを修正したプログラムです。
-
5 行目:仮引数
show_subtree
を追加する -
8 行目:
show_subtree
を同名の属性に代入する -
10 行目:
show_subtree
の値に応じて、部分木の 表示の有無を設定する
1 from marubatsu import Marubatsu_GUI
2 from tkinter import Tk
3 import os
4
5 def __init__(self, mb, params, names, ai_dict,
6 scoretable_dict, show_subtree, seed, size):
元と同じなので省略
7 self.names = names
8 self.show_subtree = show_subtree
元と同じなので省略
9 self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
10 self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "None"
11
12 Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os
def __init__(self, mb, params, names, ai_dict,
scoretable_dict, show_subtree, 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.show_subtree = show_subtree
self.seed = seed
self.size = size
super(Marubatsu_GUI, self).__init__()
from tree import Mbtree_GUI
self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "None"
Marubatsu_GUI.__init__ = __init__
修正箇所
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os
def __init__(self, mb, params, names, ai_dict,
- scoretable_dict, seed, size):
+ scoretable_dict, show_subtree, seed, size):
元と同じなので省略
self.names = names
+ self.show_subtree = show_subtree
元と同じなので省略
self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
+ self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "None"
Marubatsu_GUI.__init__ = __init__
Marubatsu クラスの play
メソッドの修正
次に、Marubatsu_GUI クラスのインスタンスを作成する Marubatsu クラスの play
メソッドを以下のプログラムのように修正します。
-
2 行目:デフォルト値を
True
とする仮引数show_subtree
を追加する -
5 行目:実引数
show_subtree=show_subtree
を追加する
1 def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None,
2 show_subtree=True, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
3 if gui:
4 mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict,
5 show_subtree=show_subtree, seed=seed, size=size)
元と同じなので省略
6
7 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None,
show_subtree=True, verbose=True, seed=None, gui=False, size=3):
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# 一部の仮引数をインスタンスの属性に代入する
self.ai = ai
self.verbose = verbose
self.gui = gui
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict,
show_subtree=show_subtree, seed=seed, size=size)
else:
mb_gui = None
self.restart()
return self.play_loop(mb_gui, params=params)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, names=None, scoretable_dict=None,
- verbose=True, seed=None, gui=False, size=3):
+ show_subtree=True, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
if gui:
mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict,
- seed=seed, size=size)
+ show_subtree=show_subtree, seed=seed, size=size)
元と同じなので省略
Marubatsu.play = play
Marubatsu_GUI クラスの create_widgets
メソッドの修正
FloatSlider を作成する必要がある ので、下記のプログラムの 6、7 行目のように create_widgets
メソッドを修正します。この処理は以前の記事と同様ですが、ゲーム盤の表示サイズを代入する Marubatsu_GUI
クラスの __init__
メソッドの 仮引数 size
のデフォルト値は 3
なので、min
を 1.0
、max
を 5.0
、step
を 0.1
としました。
1 import ipywidgets as widgets
2
3 def create_widgets(self):
元と同じなので省略
4 self.help_button = self.create_button("?", 34)
5
6 self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
7 description="size", value=self.size)
元と同じなので省略
8
9 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)
self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
description="size", value=self.size)
# 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):
元と同じなので省略
self.help_button = self.create_button("?", 34)
+ self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
+ description="size", value=self.size)
元と同じなので省略
Marubatsu_GUI.create_widgets = create_widgets
display_widgets
メソッドの修正
上記で作成した FloatSlider は、下記のプログラムの 6 行目のように、新しく作成した HBox に配置することにします。なお、次回の記事で別のウィジェットを配置する予定なので、この HBox の内容が少ない点は気にする必要はありません。
- 6 行目:FloatSlider を配置した HBox を作成する
-
8、10 行目:
hbox
の名前の末尾の数字を一つずつずらす - 12 行目:6 行目で作成した HBox を表示するように修正する
1 def display_widgets(self):
2 # 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
3 hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button,
4 self.show_tree_button, self.reset_tree_button, self.help_button])
5 # ゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
6 hbox2 = widgets.HBox([self.size_slider])
7 # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
8 hbox3 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
9 # リプレイ機能のボタンを横に配置した HBox を作成する
10 hbox4 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider])
11 # hbox1 ~ hbox4、Figure、Output を縦に配置した VBox を作成し、表示する
12 display(widgets.VBox([hbox1, hbox2, hbox3, hbox4, self.fig.canvas, self.output, self.help]))
13
14 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])
# ゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
hbox2 = widgets.HBox([self.size_slider])
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox3 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox4 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider])
# hbox1 ~ hbox4、Figure、Output を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2, hbox3, hbox4, self.fig.canvas, self.output, self.help]))
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])
# ゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
+ hbox2 = widgets.HBox([self.size_slider])
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
- hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
+ hbox3 = 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])
+ hbox4 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider])
# hbox1 ~ hbox4、Figure、Output を縦に配置した VBox を作成し、表示する
- display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output, self.help]))
+ display(widgets.VBox([hbox1, hbox2, hbox3, hbox4, self.fig.canvas, self.output, self.help]))
Marubatsu_GUI.display_widgets = display_widgets
上記の修正後に下記のプログラムを実行すると、実行結果のように FloatSlider が表示されるようになります。
gui_play()
実行結果
create_event_handler
の修正
次に、create_event_handler
を以下のプログラムのように修正します。
-
4 ~ 7 行目:「木」ボタンをクリックした際に呼び出されるイベントハンドラを定義する。行う処理は Mbtree_GUI クラスの
on_score_button_clicked
と同様だが、GUI の部分木の表示の有無の切り替えはupdate_gui
では行えないので 6 行目でshow_subtree
属性の値に応じて部分木の表示の有無の切り替える処理を行う点が異なる -
11 ~ 15 行目:FloatSlider の値を変更した際に呼び出されるイベントハンドラを定義する。行う処理は Mbtree_GUI クラスの
on_size_slider_changed
と同様だが、self.size
を幅と高さに設定している点が異なる - 17 行目:上記のイベントハンドラと FloatSlider を結び付ける
1 import math
2
3 def create_event_handler(self):
元と同じなので省略
4 def on_show_tree_button_clicked(b=None):
5 self.show_subtree = not self.show_subtree
6 self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
7 self.update_gui()
8
元と同じなので省略
9 self.help_button.on_click(on_help_button_clicked)
10
11 def on_size_slider_changed(changed):
12 self.size = changed["new"]
13 self.fig.set_figwidth(self.size)
14 self.fig.set_figheight(self.size)
15 self.update_gui()
16
17 self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
18
19 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.show_subtree = not self.show_subtree
self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
self.update_gui()
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_size_slider_changed(changed):
self.size = changed["new"]
self.fig.set_figwidth(self.size)
self.fig.set_figheight(self.size)
self.update_gui()
self.size_slider.observe(on_size_slider_changed, names="value")
# 変更ボタンのイベントハンドラを定義する
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_show_tree_button_clicked(b=None):
- self.mbtree_gui.vbox.layout.display = "none" if self.mbtree_gui.vbox.layout.display is None else None
+ self.show_subtree = not self.show_subtree
+ self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
+ self.update_gui()
元と同じなので省略
self.help_button.on_click(on_help_button_clicked)
+ def on_size_slider_changed(changed):
+ self.size = changed["new"]
+ self.fig.set_figwidth(self.size)
+ self.fig.set_figheight(self.size)
+ self.update_gui()
+ self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
update_widgets_status
メソッドの修正
「木」ボタンと「リ」ボタンが、部分木の表示の有無によって表示が変わるように update_widgets_status
メソッドを下記のプログラムのように修正します。
- 3 行目:「木」ボタンの表示の色を、部分木を表示するかどうかで変更する
- 4 行目:「リ」ボタンを、部分木を表示しない場合は操作できないようにする
1 def update_widgets_status(self):
2 self.inttext.disabled = not self.checkbox.value
3 self.set_button_color(self.show_tree_button, self.show_subtree)
4 self.set_button_status(self.reset_tree_button, not self.show_subtree)
元と同じなので省略
5
6 Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
def update_widgets_status(self):
self.inttext.disabled = not self.checkbox.value
self.set_button_color(self.show_tree_button, self.show_subtree)
self.set_button_status(self.reset_tree_button, not self.show_subtree)
self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
self.set_button_status(self.first_button, self.mb.move_count <= 0)
self.set_button_status(self.prev_button, self.mb.move_count <= 0)
self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)
# value 属性よりも先に max 属性に値を代入する必要がある点に注意!
self.slider.max = len(self.mb.records) - 1
self.slider.value = self.mb.move_count
Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
def update_widgets_status(self):
self.inttext.disabled = not self.checkbox.value
+ self.set_button_color(self.show_tree_button, self.show_subtree)
+ self.set_button_status(self.reset_tree_button, not self.show_subtree)
元と同じなので省略
Marubatsu_GUI.update_widgets_status = update_widgets_status
実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、以下の確認を行ってください。
- FloatSlider を操作することで ゲーム盤の表示の大きさが変更される
- 「木」ボタンをクリックして GUI の部分木の表示の有無を変更すると、「木」ボタンの色が変わる
- 「木」ボタンをクリックして GUI の部分木の表示を消すと、「リ」ボタンが操作できなくなる
update_gui()
文字の表示の大きさの修正
上記の修正によって、ゲーム盤の表示の大きさを変更できるようになりましたが、ゲーム盤の大きさを変更しても表示される 文字の大きさが変わらない という問題があります。そのため、例えばゲーム盤のサイズを 1.5 にすると、下図のように 文字がはみ出て表示される ようになります。
この問題を修正するためには、以前の記事で説明したように、文字の大きさ を ゲーム盤の大きさ に 比例した大きさで表示する ように修正する必要があります。
ゲーム盤の文字の表示は Marubatsu_GUI クラスの update_gui
メソッドで行われるので、文字を表示する処理を下記のプログラムのように修正します。
-
3、4 行目:元のプログラムでは
fontsize
は 20 に設定 されており、self.size
には 初期設定では3
が代入 されるので、fontsize
は20 / 3 * self.size
という式で計算することができるが、20 / 3
は割り切れないので、7 * self.size
に修正した
1 def update_gui(self):
元と同じなので省略
2 ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}",
3 fontsize=7*self.size, ha="center")
元と同じなので省略
4 ax.text(0, -0.2, text, fontsize=7*self.size)
元と同じなので省略
5
6 Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
ax = self.ax
ai = self.mb.ai
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# リプレイ中、ゲームの決着がついていた場合は背景色を変更する
is_replay = self.mb.move_count < len(self.mb.records) - 1
if self.mb.status == Marubatsu.PLAYING:
facecolor = "lightcyan" if is_replay else "white"
else:
facecolor = "lightyellow"
ax.figure.set_facecolor(facecolor)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}",
fontsize=7*self.size, ha="center")
# ゲームの決着がついていない場合は、手番を表示する
if self.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
# リプレイ中の場合は "Replay" を表示する
if is_replay:
text += " Replay"
ax.text(0, -0.2, text, fontsize=7*self.size)
self.draw_board(ax, self.mb)
self.update_widgets_status()
if hasattr(self, "mbtree_gui"):
from tree import Node
self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
self.mbtree_gui.update_gui()
Marubatsu_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}",
- fontsize=20, ha="center")
+ fontsize=7*self.size, ha="center")
元と同じなので省略
- ax.text(0, -0.2, text, fontsize=20)
+ ax.text(0, -0.2, text, fontsize=7*self.size)
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
上記の修正後に下記のプログラムを実行し、ゲーム盤の大きさを変更すると実行結果のように、文字の大きさも変更されることが確認できます。
gui_play()
枠の太さの表示の修正
ゲーム盤の大きさを小さくすると、上図のように 枠線が太くなるように見える という現象が発生します。これは、matplotlib では、線の太さ は文字の大きさと同様に、Figure の大きさや Axes の表示範囲を変えても 変わらないことが原因 です。
ゲーム盤に表示する 線の太さ は draw_board
メソッドの 仮引数 lw
に代入 することで指定できるので、下記のプログラムのように修正することで、ゲーム盤の表示の大きさにあわせて線の太さが変わるようになります。
-
2 行目:
draw_board
にキーワード引数lw=0.7*self.size
を記述する。仮引数lw
のデフォルト値が2
なので、このような式にした
1 def update_gui(self):
元と同じなので省略
2 self.draw_board(ax, self.mb, lw=0.7*self.size)
元と同じなので省略
3
4 Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
ax = self.ax
ai = self.mb.ai
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# リプレイ中、ゲームの決着がついていた場合は背景色を変更する
is_replay = self.mb.move_count < len(self.mb.records) - 1
if self.mb.status == Marubatsu.PLAYING:
facecolor = "lightcyan" if is_replay else "white"
else:
facecolor = "lightyellow"
ax.figure.set_facecolor(facecolor)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}",
fontsize=7*self.size, ha="center")
# ゲームの決着がついていない場合は、手番を表示する
if self.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
# リプレイ中の場合は "Replay" を表示する
if is_replay:
text += " Replay"
ax.text(0, -0.2, text, fontsize=7*self.size)
self.draw_board(ax, self.mb, lw=0.7*self.size)
self.update_widgets_status()
if hasattr(self, "mbtree_gui"):
from tree import Node
self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
self.mbtree_gui.update_gui()
Marubatsu_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
self.draw_board(ax, self.mb, lw=0.7*self.size)
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
上記の修正後に下記のプログラムを実行し、ゲーム盤の大きさを変更すると実行結果のように、文字の大きさも変更されることが確認できます。
gui_play()
今回の記事のまとめ
今回の記事では、リプレイ機能を利用した際のバグの検証と修正を行いました。また、Marubatsu_GUI クラスのいくつかの改良を行いました。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu_new.py | 今回の記事で更新した marubatsu.py |
tree_new.py | 今回の記事で更新した tree.py |
次回の記事