目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
AI が手番を担当した場合のリプレイ機能の実装
前回の記事では、リプレイ機能を実装する際に、人間どうしの対戦を行いましたが、AI が手番を担当した場合 では、この後で示すように リプレイ機能がうまく働きません。
例えば、下記のプログラムで gui_play
を実行して AI どうしの対戦を GUI で行う と、実行結果のように、すべてのリプレイのボタンが灰色で表示 され、リプレイの操作ができない という問題が発生します。なお、対戦する AI はどの AI でも構いません。このような問題が起きる原因について少し考えてみて下さい。
from util import gui_play
from ai import ai1
gui_play(ai=[ai1, ai1])
実行結果(下図は、画像なので操作することはできません)
問題の原因の検証
リプレイボタンの設定 は、update_widgets_status
を呼び出すことで行いますが、この処理は現時点では下記の場合でのみ行われています。
-
play
メソッド内で、restart
メソッドを呼び出してゲームをリセットした直後 - リセットボタンをクリックした時のイベントハンドラの中
- ゲーム盤の上でマウスを押した時のイベントハンドラの中で、着手を行った場合
- リプレイボタンをクリックした際に呼び出される
change_step
の中
update_widgets_status
がどこで呼び出されているかを確認する場合は、VSCode の Ctrl + F による検索機能 を利用すると良いでしょう。
また、AI どうしが対戦を行う場合は以下のような処理が行われます。
-
play
メソッドの実行または、リセットボタンをクリックした際にゲームが開始される - いずれの場合も
update_widgets_status
が呼び出され、ゲームの開始時は 0 手目の局面なので、すべてのリプレイに関するボタンが灰色で表示 され、操作ができなくなる - その後、
play_loop
メソッドによって AI どうしの対戦が決着がつくまで行われる - ゲームの決着後に
draw_board
メソッドが呼び出され、決着後のゲーム盤が描画される - ゲームの開始から決着がつくまでの間に
update_widgets_status
が 呼び出されることはない ので、すべてのリプレイに関するボタン は 灰色で表示されたまま になる
これが AI どうしで対戦を行った際にリプレイ機能が利用できない原因です。
update_widgets_status
を呼び出す必要がある場所の検証
これまでは、リプレイ機能に関するボタンの設定の更新を行う必要がある状況が 新しく判明した時点 で、その都度 必要な場所に update_widgets_status
を記述してきました 。
このような、プログラムで 特定の処理を行う必要がある場合 に、その処理を行う必要がある場面が 見つかってからその処理を記述する という 場当たり的な方法 は、実際に良く行われる方法ですが、その方法では、以下の 2 つが区別できない という問題があります。
- プログラムに特定の処理を記述する必要がある場所がもう 存在しない
- プログラムに特定の処理を記述する必要がある場所が 存在するが、見つかっていない
もちろん、特定の処理をどこに記述する必要があるかが良くわかっていない場合は、見つかってから対処するしかないのですが、特定の処理を どこに記述する必要があるかがはっきりとわかる場合 は、その場所を明確にすることで、必要な場所に確実にその処理を記述できる ようになります。
そこで、リプレイ機能に関する ボタンの設定の更新を必ず行う必要がある状況を検証 することにします。リプレイ機能に関するボタンの設定の更新を必ず行う必要がある状況が何であるかについて少し考えてみて下さい。
特定の処理を行う必要がある場所がはっきりしないという状況がピンとこない人は、特定の処理を「バグを修正する」だと考えてみて下さい。
プログラムにはバグはつきものですが、プログラムが長くなればなるほど、プログラムの中に潜むバグを見つけることは困難になります。例えば 1 万行のプログラムの中に潜むバグを、プログラムを 1 行ずつ確認して探し出すことは大変です。
そのため、プログラムの バグの多く は、バグが発生してから原因を検証して対処する という方法を 取らざるを得ません。実際に本記事でもこれまで何度もバグが発生してから対処するという方法を取ってきました。
一方、特定の処理を行った場合に発生するバグ に関しては、プログラムがどれだけ長くても、その処理に関連する部分だけに注目 してバグを修正することができます。ただし、関連する部分が多い場合、やはりバグを見つけるのは困難です。
下記の update_widgets_status
が 4 つのリプレイに関するボタンに対して行う処理は、self.move_count <= 0
と self.move_count >= len(self.board_records) - 1
の式からわかるように、いずれも 現在の手数 を表す self.move_count
の値に応じてボタンの設定を変更 する 処理を行います。従って、リプレイ機能に関するボタンの設定は、ゲームの手数が変更された際に行う 必要があります。
def update_widgets_status():
set_button_status(first_button, self.move_count <= 0)
set_button_status(prev_button, self.move_count <= 0)
set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
上記から、ゲームの手数を変更する処理を探し出し、その直後に update_widgets_status
を呼び出す処理を記述すれば良いことがわかりますが、もっと簡単な方法があるのでその方法を少し考えてみて下さい。
ゲームの手数が変更された場合 は、必ず ゲーム盤の表示を更新する必要がある ので、その処理を行う draw_board
メソッドが 呼び出す必要があります。つまり、draw_board
メソッドの中に update_widgets_status
を呼び出す処理を記述する ことで、ゲームの手数が変更された場合に必ず update_widgets_status
が呼び出されるようになります。
細かい話になりますが、手数を変更する処理の後に毎回 update_widgets_status
を記述するよりも、draw_board
の中に update_widgets_status
を記述したほうが良い理由が他にもあります。それは、AI どうしの対戦を行う場合です。
AI どうしの対戦 が行われた場合は、すぐに決着がつくため、対戦の 途中経過のゲーム盤の画面を描画 しても 対戦結果のゲーム盤の描画で上書き されてしまいます。そのため、play_loop
の下記のプログラムの 9、10 によって、必要のない途中経過の画面を描画しない という処理を行っています。
1 # ゲームの決着がついていない間繰り返す
2 while self.status == Marubatsu.PLAYING:
3 # 現在の手番を表す ai のインデックスを計算する
4 index = 0 if self.turn == Marubatsu.CIRCLE else 1
5 # ゲーム盤の表示
6 if verbose:
7 if gui:
8 # AI どうしの対戦の場合は画面を描画しない
9 if ai[0] is None or ai[1] is None:
10 self.draw_board(ax, ai)
略
これは、リプレイに関するボタンの設定も同様で、AI どうしの対戦の 途中の局面 に対して、リプレイに関するボタンの設定を行う処理 を行っても、すぐに決着がついた局面でのリプレイボタンの設定で上書きされてしまうため 意味はありません。draw_board
の中で update_widgets_status
を呼び出すようにすることで、対戦の途中で 無駄な update_widgets_status
の呼び出しが行われなくなります。
draw_board
メソッドからの update_widgets_status
の呼び出し
上記の処理は、下記のプログラムの 10 行目のように、draw_board
のブロックの中の最後に update_widgets_status
の呼び出しを記述すれば良いと思う人がいるかもしれません。
1 from marubatsu import Marubatsu
2
3 def draw_board(self, ax, ai):
元と同じなので省略
4 # ゲーム盤のマークを描画する
5 for y in range(self.BOARD_SIZE):
6 for x in range(self.BOARD_SIZE):
7 color = "red" if (x, y) == self.last_move else "black"
8 self.draw_mark(ax, x, y, self.board[x][y], color)
9
10 update_widgets_status()
11
12 Marubatsu.draw_board = draw_board
行番号のないプログラム
from marubatsu import Marubatsu
def draw_board(self, ax, ai):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を
facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
ax.figure.set_facecolor(facecolor)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
names = []
for i in range(2):
names.append("人間" if ai[i] is None else ai[i].__name__)
ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 引き分けの場合
elif self.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
update_widgets_status()
Marubatsu.draw_board = draw_board
修正箇所
from marubatsu import Marubatsu
def draw_board(self, ax, ai):
元と同じなので省略
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
+ update_widgets_status()
Marubatsu.draw_board = draw_board
しかし、上記の修正後に、下記のプログラムで gui_play
を実行すると、実行結果のようなエラーが発生します。このエラーの原因について少し考えてみて下さい。
gui_play(ai=[ai1, ai1])
実行結果
略
Cell In[2], line 46
43 color = "red" if (x, y) == self.last_move else "black"
44 self.draw_mark(ax, x, y, self.board[x][y], color)
---> 46 update_widgets_status()
NameError: name 'update_widgets_status' is not defined
エラーメッセージから、update_widgets_status
が定義されていない ことがわかります。これは、update_widgets_status
が play
メソッドのローカル関数として定義されている ため、play
メソッドの外 の draw_board
メソッドでは 利用できない からです。
以前の記事でも説明しましたが、定義されていない名前は VSCode では下図のようにオレンジ色の波線が表示されます。
そこで、下記のプログラムのように、update_widgets_status
の定義 を、play
メソッドの中にコピーすればよい と思う人がいるかもしれません。なお、update_widgets_status
の中で、set_button_status
を呼び出しているので、set_button_status
もコピーしました。
1 def draw_board(self, ax, ai):
元と同じなので省略
2 # ボタンのウィジェットの状態を設定する
3 def set_button_status(button, disabled):
4 button.disabled = disabled
5 button.style.button_color = "lightgray" if disabled else "lightgreen"
6
7 # ウィジェットの状態を更新する
8 def update_widgets_status():
9 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
10 set_button_status(first_button, self.move_count <= 0)
11 set_button_status(prev_button, self.move_count <= 0)
12 set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
13 set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
14
15 update_widgets_status()
16
17 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, ax, ai):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を
facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
ax.figure.set_facecolor(facecolor)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
names = []
for i in range(2):
names.append("人間" if ai[i] is None else ai[i].__name__)
ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 引き分けの場合
elif self.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
button.disabled = disabled
button.style.button_color = "lightgray" if disabled else "lightgreen"
# ウィジェットの状態を更新する
def update_widgets_status():
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
set_button_status(first_button, self.move_count <= 0)
set_button_status(prev_button, self.move_count <= 0)
set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
update_widgets_status()
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, ax, ai):
元と同じなので省略
# ボタンのウィジェットの状態を設定する
+ def set_button_status(button, disabled):
+ button.disabled = disabled
+ button.style.button_color = "lightgray" if disabled else "lightgreen"
# ウィジェットの状態を更新する
+ def update_widgets_status():
+ # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
+ set_button_status(first_button, self.move_count <= 0)
+ set_button_status(prev_button, self.move_count <= 0)
+ set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
+ set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
update_widgets_status()
Marubatsu.draw_board = draw_board
上記の修正後に、下記のプログラムで gui_play
を実行すると、今度は実行結果のような別のエラーが発生します。このエラーの原因について少し考えてみて下さい。
gui_play(ai=[ai1, ai1])
実行結果
略
Cell In[4], line 52
50 def update_widgets_status():
51 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
---> 52 set_button_status(first_button, self.move_count <= 0)
53 set_button_status(prev_button, self.move_count <= 0)
54 set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
NameError: name 'first_button' is not defined
エラーメッセージから、今度は first_button
が定義されていない ことがわかります。これは、先程と同様に、first_button
が play
メソッドのローカル変数として定義されている からです。この問題を解決する方法について少し考えてみて下さい。
属性によるデータの共有
first_button
に関する処理を draw_board
メソッドの中に移動すればよいと思う人がいるかもしれませんが、first_button
は、play
メソッドの中で利用されている ので、first_button
に関する処理を draw_board
メソッドに移動すると今度は play
メソッドで first_button
を利用する処理が動作しなくなる という問題が発生します。
これまで、update_widgets_status
の中で first_button
をそのまま利用できていた のは、update_widgets_status
が、first_button
をローカル変数として持つ play
メソッドのローカル関数として定義されていたから です。従って、draw_board
メソッドを、play
メソッドのローカル関数として定義すれば良いと思った人がいるかもしれませんが、その方法はうまくいきません。その理由は、draw_board
が play_loop
という、play
メソッド以外のメソッドから呼び出されている ため、draw_board
メソッドを、play
メソッドのローカル関数として定義すると、play_loop
から draw_board
を利用できなくなるからです。
この問題を解決する方法の一つは、first_button
をローカル変数ではなく、Marubatsu
クラスのインスタンスの属性とする というものです。そのようにすることで、play
メソッドと draw_board
メソッド内で、self.first_button
と記述することで、self.first_button
のボタンのウィジェットを共有することができるようになります。
下記は、play
メソッドの修正です。なお、set_button_status
と update_widgets_status
は draw_board
の中だけでしか利用しないので、それらの関数の定義を play
メソッドの中から削除しました。また、update_widget_status
の呼び出しは draw_board
の中で行うようにしたため、必要がなくなったので play
メソッドの中から削除しました。
-
12 行目の下:
set_button_status
とupdate_widgets_status
の 定義を削除 する -
16、28、43、45行目の下:
update_widgets_status
の 呼び出しを削除 する -
21 ~ 24、29 ~ 32、37 行目:
first_button
などの、ボタンのウィジェットを表すローカル変数の前にself.
を記述して、インスタンスの属性に修正 する
1 import matplotlib.pyplot as plt
2 import ipywidgets as widgets
3 import math
4 from copy import deepcopy
5
6 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
7 # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
8 if gui:
9 # %matplotlib widget のマジックコマンドを実行する
10 get_ipython().run_line_magic('matplotlib', 'widget')
11
12 # この下にあった set_button_status と update_widgets_status の定義を削除する
元と同じなので省略
13 # リセットボタンのイベントハンドラを定義する
14 def on_reset_button_clicked(b):
15 self.restart()
16 # この下にあった update_widgets_status の呼び出しを削除する
17
18 on_change_button_clicked(b)
元と同じなので省略
19 # 2 行目の UI を作成する
20 # リプレイのボタンを作成する
21 self.first_button = create_button("<<", 100)
22 self.prev_button = create_button("<", 100)
23 self.next_button = create_button(">", 100)
24 self.last_button = create_button(">>", 100)
元と同じなので省略
25 def change_step(step):
元と同じなので省略
26 # 描画を更新する
27 self.draw_board(ax, ai)
28 # この下にあった update_widgets_status の呼び出しを削除する
元と同じなので省略
29 self.first_button.on_click(on_first_button_clicked)
30 self.prev_button.on_click(on_prev_button_clicked)
31 self.next_button.on_click(on_next_button_clicked)
32 self.last_button.on_click(on_last_button_clicked)
33
34 # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
35 hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
36 # リプレイ機能のボタンを横に配置した HBox を作成する
37 hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button])
38 # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
39 display(widgets.VBox([hbox1, hbox2]))
元と同じなので省略
40 # ローカル関数としてイベントハンドラを定義する
41 def on_mouse_down(event):
元と同じなので省略
42 self.draw_board(ax, ai)
43 # この下にあった update_widgets_status の呼び出しを削除する
元と同じなので省略
44 self.restart()
45 # この下にあった update_widgets_status の呼び出しを削除する
46
47 return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
48
49 Marubatsu.play = play
行番号のないプログラム
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math
from copy import deepcopy
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
style={"button_color": "lightgreen"},
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
self.first_button = create_button("<<", 100)
self.prev_button = create_button("<", 100)
self.next_button = create_button(">", 100)
self.last_button = create_button(">>", 100)
def change_step(step):
# step が負の場合は 0 に修正する
step = max(0, min(len(self.board_records) - 1, step))
# step 手目のゲーム盤のデータをコピーし、board に代入する
self.board = deepcopy(self.board_records[step])
# 手数を表す step を move_count に代入する
self.move_count = step
# 手番を計算する。step が偶数の場合は 〇 の 手番
self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
# status 属性を judget を使って計算する
self.status = self.judge()
# 直前の着手を計算する
self.last_move = self.records[step]
# 描画を更新する
self.draw_board(ax, ai)
def on_first_button_clicked(b):
change_step(0)
def on_prev_button_clicked(b):
change_step(self.move_count - 1)
def on_next_button_clicked(b):
change_step(self.move_count + 1)
def on_last_button_clicked(b):
change_step(len(self.board_records) - 1)
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)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math
from copy import deepcopy
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# ボタンのウィジェットの状態を設定する
- def set_button_status(button, disabled):
- button.disabled = disabled
- button.style.button_color = "lightgray" if disabled else "lightgreen"
# ウィジェットの状態を更新する
- def update_widgets_status():
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
- set_button_status(first_button, self.move_count <= 0)
- set_button_status(prev_button, self.move_count <= 0)
- set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
- set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
元と同じなので省略
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
- update_widgets_status()
on_change_button_clicked(b)
元と同じなので省略
# 2 行目の UI を作成する
# リプレイのボタンを作成する
- first_button = create_button("<<", 100)
+ self.first_button = create_button("<<", 100)
- prev_button = create_button("<", 100)
+ self.prev_button = create_button("<", 100)
- next_button = create_button(">", 100)
+ self.next_button = create_button(">", 100)
- last_button = create_button(">>", 100)
+ self.last_button = create_button(">>", 100)
元と同じなので省略
def change_step(step):
元と同じなので省略
# 描画を更新する
self.draw_board(ax, ai)
- update_widgets_status()
元と同じなので省略
- first_button.on_click(on_first_button_clicked)
+ self.first_button.on_click(on_first_button_clicked)
- prev_button.on_click(on_prev_button_clicked)
+ self.prev_button.on_click(on_prev_button_clicked)
- next_button.on_click(on_next_button_clicked)
+ self.next_button.on_click(on_next_button_clicked)
- last_button.on_click(on_last_button_clicked)
+ self.last_button.on_click(on_last_button_clicked)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
- hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
+ hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
元と同じなので省略
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
元と同じなので省略
self.draw_board(ax, ai)
- update_widgets_status()
元と同じなので省略
self.restart()
- update_widgets_status()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
下記は、draw_board
メソッドを修正したプログラムです。
-
5 ~ 8 行目:
first_button
などのボタンのウィジェットが代入されているローカル変数の前に、self.
を記述してインスタンスの属性に修正する
1 def draw_board(self, ax, ai):
元と同じなので省略
2 # ウィジェットの状態を更新する
3 def update_widgets_status():
4 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
5 set_button_status(self.first_button, self.move_count <= 0)
6 set_button_status(self.prev_button, self.move_count <= 0)
7 set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
8 set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
9
10 update_widgets_status()
11
12 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, ax, ai):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を
facecolor = "white" if self.status == Marubatsu.PLAYING else "lightyellow"
ax.figure.set_facecolor(facecolor)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
names = []
for i in range(2):
names.append("人間" if ai[i] is None else ai[i].__name__)
ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 引き分けの場合
elif self.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.BOARD_SIZE):
ax.plot([0, self.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
color = "red" if (x, y) == self.last_move else "black"
self.draw_mark(ax, x, y, self.board[x][y], color)
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
button.disabled = disabled
button.style.button_color = "lightgray" if disabled else "lightgreen"
# ウィジェットの状態を更新する
def update_widgets_status():
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
set_button_status(self.first_button, self.move_count <= 0)
set_button_status(self.prev_button, self.move_count <= 0)
set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
update_widgets_status()
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, ax, ai):
元と同じなので省略
# ウィジェットの状態を更新する
def update_widgets_status():
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
- set_button_status(first_button, self.move_count <= 0)
+ set_button_status(self.first_button, self.move_count <= 0)
- set_button_status(prev_button, self.move_count <= 0)
+ set_button_status(self.prev_button, self.move_count <= 0)
- set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
+ set_button_status(self.next_button, self.move_count >= len(self.board_records) - 1)
- set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
+ set_button_status(self.last_button, self.move_count >= len(self.board_records) - 1)
update_widgets_status()
Marubatsu.draw_board = draw_board
上記の修正後に、下記のプログラムで gui_play
を実行して AI どうしの対戦を GUI で行うと、実行結果のように << と < ボタンが緑色で表示され、リプレイ機能を利用できるようになります。実際にリプレイ機能のボタンをクリックして確認して下さい。
gui_play(ai=[ai1, ai1])
実行結果(下図は、画像なので操作することはできません)
また、図は省略しますが、人間 VS AI と AI VS 人間でもリプレイ機能を利用できるようになったことを確認して下さい。
クラスによる GUI の機能の分離
ここまでで、リプレイ機能の主要な機能は完成しましたが、play
メソッドの内容がかなり長く、複雑になりすぎてプログラムがわかりづらくなったと感じている人が多いかもしれません。また、以下のような点が気になっている人はいないでしょうか?
-
first_button
などのリプレイに関するボタンはインスタンスのメソッドに代入されているが、reset_button
などのボタンはあいかわらずローカル変数に代入されている - GUI に関する多くの関数は、
play
メソッドのローカル関数として定義されているが、draw_board
はMarubatsu
クラスのメソッドとして定義されている
上記のような、統一性がないプログラム は、バグの原因になる可能性が高くなる ので、避けたほうが良い でしょう。
また、これまでのプログラムで、first_button
や update_widgets_status
などを、Marubatsu
クラスの属性やメソッドではなく、play
メソッドのローカル変数やローカル関数としてきたのは、以下のような理由からでした。
- それらが
play
メソッドの中でしか利用されないデータや関数であったため -
Marubatsu
クラスの属性やメソッドを増やすと、クラスの構成が複雑になり、わかりづらくなるため
GUI に関する処理が少なければローカル変数やローカル関数で処理を記述してもあまり問題はありませんが、現状の play
メソッドのように長く、複雑になった場合は、もっとわかりやすい方法で記述したほうが良いでしょう。
プログラミムの記述の方法に、唯一の正解はありません。また、人によってわかりやすいプログラムは異なります。データや関数をローカル変数やローカル関数で記述するか、属性やメソッドで記述するかは、一長一短があるので、どちらかが必ずしも優れているとは限らない点に注意して下さい。
本記事で行ってきたように、必要な機能を、必要になった時点で実装していく という方法でプログラムを記述すると、どうしてもプログラムの変数や関数などの構成が複雑になったり、統一性がなくなってわかりづらくなることが良くあります。そのような場合は、プログラムの構成を整理して作り直すことが良く行われます。そのようなことを体験することは、プログラミングの能力の向上につながると思いますので実例を紹介することにします。
現実の世界で例えると、例えば自分の部屋の本棚に購入した本を入れる場合、本の数が少ないうちは、その場でその本を入れる場所を考えて入れても大きな問題は発生しないでしょう。しかし、本の数がある程度以上増えてくると、本の配置に矛盾が生じたりしてわかりづらくなるため、本の配置を整理し直す必要が生じるでしょう。
〇×ゲームの場合は、AI で対戦を行う機能を実装後に、play
メソッド内に GUI に関する処理を追加して記述しましたが、GUI に関する機能 は、〇×ゲームを AI で遊ぶ際に必要となる 必須の機能ではありません。また、GUI に関する処理を Marubatsu
クラスの中に記述すると、必須の機能とそうでない機能が混ざってわかりづらくなります。そこで、Marubatsu
クラスの中から GUI に関する処理を分離する ことで、Marubatsu
クラスの 構成をシンプルにしてわかりやすくする ことにします。具体的には、GUI に関する処理 を 別のクラスを定義して記述する ことで、Marubatsu
クラスから分離する ことにします。
なお、関数ではなく、クラスという形で分離することにしたのは、以下の理由からです。
- GUI に関する処理は、
play
メソッドで行うウィジェットの定義や配置などに関する処理と、draw_board
メソッドで行うウィジェットの表示に関する処理がある。それらの処理を クラスのメソッドとして定義する 事で、まとめて扱うことができる ようになる - ウィジェットの定義や配置などに関する処理と、ウィジェットの表示に関する処理では、AI やウィジェットの情報などの 共通する情報を扱う必要がある。それぞれを関数で定義すると、共通する情報を仮引数に代入する必要が生じるが、クラスのメソッドとして定義した場合は、それらの情報を属性に代入 することで、仮引数が必要なくなる
後者の理由は、言葉だけの説明ではわかりづらいと思いますが、この後で行う実装を見れば、意味がわかるようになるのではないかと思います。
Marubatsu_GUI
クラスの記述の方針
作成するクラスの名前は、〇×ゲームの GUI の処理を行うので、Marubatsu_GUI
とします。
クラスには、play
メソッド内に記述されていた GUI に関する処理を記述 しますが、play
メソッドと同じように記述するとわかりづらいので、下記の方針で記述することにします。
- ウィジェットの作成、ウィジェットの配置、ウィジェットの表示の更新、イベントハンドラの定義と結び付け などの処理を行う メソッドをそれぞれ定義 する。そうする事で、プログラムの処理が整理 されて、わかりやすくなるという効果が得られる
- 必要がない限り、ローカル変数やローカル関数を使わずに、属性とメソッドを利用 する
-
draw_board
メソッドは GUI に関する処理 なのでMarubatsu_GUI
のメソッド にする
__init__
の定義
まず、インスタンスを作成した際に実行される __init__
メソッドの定義を行います。そのためには、__init__
の 仮引数を決める必要 があります。もともと GUI の処理は Marubatsu
クラスの play
メソッド内で行われていた処理なので、Marubatsu_GUI
クラスが行う GUI の処理に必要となるデータ は、play
メソッドの仮引数の一部 です。従って、__init__
メソッドの仮引数に必要となる、play
メソッドの仮引数を検証する ことにします。
play
メソッドの仮引数と、Marubatsu_GUI
での必要性は以下の通りです。なお、必要性が × となっている仮引数が、今後必要になった場合は、その都度追加することにします。
仮引数 | 意味 | 必要性 | 理由 |
---|---|---|---|
self |
Marubatsu クラスのインスタンス |
〇 | 様々な場面で必要 |
ai |
手番を担当する AI の list | 〇 | AI の処理を行う際に必要 |
ai_dict |
Dropdown の AI の一覧 | 〇 | Dropdown の作成に必要 |
params |
AI の関数に渡すパラメータ | 〇 | AI の処理を行う際に必要 |
verbose |
途中経過を表示するか | × | GUI で遊ぶ場合は必ずTrue になるので不要 |
seed |
乱数の種 | × | 乱数の種の処理は GUI とは関係がないので不要 |
gui |
GUI で遊ぶか | × | GUI で遊ぶ場合は必ずTrue になるので不要 |
size |
ゲーム盤の画像のサイズ | 〇 | ゲーム盤の描画の際に必要 |
なお、play
メソッドの self
は Marubatsu
クラスのインスタンス を表しますが、上記の __init__
のメソッドの self
は Marubatsu_GUI
クラスのインスタンス を表すので、上記の表の self
を代入する __init__
メソッドの 仮引数の名前 は self
以外の名前 にする必要があります。そこで、本記事では mb
という名前にする ことにします。
下記は、Marubatsu_GUI
クラスと、__init__
メソッドの定義です。__init__
メソッドには、play
メソッドの先頭に記述されていた処理の中で、GUI に関する処理の一部を記述しました。なお、乱数の種に関する処理は GUI には関係がないので記述していません。
-
2 行目:上記の表の仮引数を記述する。デフォルト引数のデフォルト値は
play
メソッドと同じ値を設定した -
4 ~ 8 行目:
play
メソッド内の、仮引数ai_dict
、params
に関する処理を記述する -
10 ~ 14 行目:
__init__
メソッドの 仮引数 は、一般的にそのクラスの さまざまな場所で使われるデータ なので、下記のプログラムの 10 ~ 14 行目のように、それぞれの仮引数の値 を、同じ名前の属性に代入するのが一般的 である。そのようにすることで、クラスのメソッド でそれらの値を 利用できるようになる -
17 行目:
play
メソッド内で GUI の処理が記述されている、if gui:
のブロックの最初で行われている、%matplotlib widget
のマジックコマンドを実行する処理を記述する。なお、それ以降の処理は、この後で順次追加する
1 class Marubatsu_GUI:
2 def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
3 # ai_dict が None の場合は、空の list で置き換える
4 if ai_dict is None:
5 ai_dict = {}
6 # params が None の場合のデフォルト値を設定する
7 if params is None:
8 params = [{}, {}]
9
10 self.mb = mb
11 self.ai = ai
12 self.ai_dict = ai_dict
13 self.ai_params = params
14 self.size = size
15
16 # %matplotlib widget のマジックコマンドを実行する
17 get_ipython().run_line_magic('matplotlib', 'widget')
行番号のないプログラム
class Marubatsu_GUI:
def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
self.mb = mb
self.ai = ai
self.ai_dict = ai_dict
self.ai_params = params
self.size = size
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
上記の 4、5、12 行目は、下記のプログラムのように 1 行で記述することもできます。7、8、13 行目も同様です。わかりやすいと思ったほうを採用して下さい。
self.ai_dict = {} if ai_dict is None else ai_dict
下記の Marubatsu
クラスの __init__
メソッドでも、上記と同様に、仮引数 board_size
を同じ名前の属性に代入しています。なお、BOARD_SIZE
属性を大文字にしているのは、以前の記事で説明したように、後から BOARD_SIZE
属性に代入された値を変更する予定がない定数であることを明確にするためです。
class Marubatsu:
def __init__(self, board_size=3):
# ゲーム盤の縦横のサイズ
self.BOARD_SIZE = board_size
# 〇×ゲーム盤を再起動するメソッドを呼び出す
self.restart()
AI を選択する Dropdown を作成する関数の定義
play
メソッドで次に記述されているのは、AI を選択する Dropdown を作成する処理 です。そこで、Dropdown を作成する create_dropdown
を下記のプログラムのように定義します。基本的には play
メソッド内の処理をコピー しますが、以下の点が異なります。なお、Dropdown を作成するために 必要なデータ は、__init__
メソッド内で、インスタンスの属性に代入済 なので、仮引数 に self
以外を記述する必要はありません。これが、先程説明した、GUI の機能をクラスで定義する事の 2 つ目の理由です。
- 5、24 行目:ウィジェットをローカル変数ではなく、インスタンスの属性に代入する
-
9、13、14、18、20、26 行目:
ai
等のローカル変数をself.ai
に変更する
1 def create_dropdown(self):
2 # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
3 select_values = []
4 # 〇 と × の Dropdown を格納する list
5 self.dropdown_list = []
6 # ai に代入されている内容を ai_dict に追加する
7 for i in range(2):
8 # ラベルと項目の値を計算する
9 if self.ai[i] is None:
10 label = "人間"
11 value = "人間"
12 else:
13 label = self.ai[i].__name__
14 value = self.ai[i]
15 # value を select_values に常に登録する
16 select_values.append(value)
17 # value が ai_values に登録済かどうかを判定する
18 if value not in self.ai_dict.values():
19 # 項目を登録する
20 self.ai_dict[label] = value
21
22 # Dropdown の description を計算する
23 description = "〇" if i == 0 else "×"
24 self.dropdown_list.append(
25 widgets.Dropdown(
26 options=self.ai_dict,
27 description=description,
28 layout=widgets.Layout(width="100px"),
29 style={"description_width": "20px"},
30 value=select_values[i],
31 )
32 )
33
34 Marubatsu_GUI.create_dropdown = create_dropdown
行番号のないプログラム
def create_dropdown(self):
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
self.dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if self.ai[i] is None:
label = "人間"
value = "人間"
else:
label = self.ai[i].__name__
value = self.ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in self.ai_dict.values():
# 項目を登録する
self.ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
self.dropdown_list.append(
widgets.Dropdown(
options=self.ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
Marubatsu_GUI.create_dropdown = create_dropdown
修正箇所
def create_dropdown(self):
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
- dropdown_list = []
+ self.dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
- if ai[i] is None:
+ if self.ai[i] is None:
label = "人間"
value = "人間"
else:
- label = ai[i].__name__
+ label = self.ai[i].__name__
- value = ai[i]
+ value = self.ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
- if value not in ai_dict.values():
+ if value not in self.ai_dict.values():
# 項目を登録する
- ai_dict[label] = value
+ self.ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
- dropdown_list.append(
+ self.dropdown_list.append(
widgets.Dropdown(
- options=ai_dict,
+ options=self.ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
Marubatsu_GUI.create_dropdown = create_dropdown
下記のプログラムの 2、3 行目のように、self.ai
などをローカル変数に代入することで、ai
と ai_dict
を self.ai
のように修正する必要が無くなります。
def create_dropdown(self):
ai = self.ai
ai_dict = self.ai_dict
以下略
ただし、例えば self.ai = None
などのように、self.ai
や self.ai_dict
に 直接値を代入する処理 を この後のプログラムで記述する必要がある場合 は、ai
と self.ai
の 値が別のデータになってしまう ため バグが発生する可能性が生じる 点に注意して下さい。
なお、self.ai[0] = None
のように、要素の値を変更する場合は、ai[0]
の値も同時に変更されるので問題はありません。
上記の区別がついていない方は、このような修正は避けたほうが良いでしょう。
ウィジェットを配置して表示する関数の定義
上記の関数はウィジェットを作成するだけなので、ウィジェットを配置して表示 する、display_widgets
という関数を下記のプログラムのように定義します。なお、現時点では、Dropdown しか作成していないので Dropdown を配置して表示する処理だけを記述します。
def display_widgets(self):
hbox = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1]])
display(hbox)
Marubatsu_GUI.display_widgets = display_widgets
次に、下記のプログラムのように、__init__
メソッドの最後の 5、6 行目に Dropdown を作成し、ウィジェットを表示するプログラムを記述します。
1 def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
元と同じなので省略
2 # %matplotlib widget のマジックコマンドを実行する
3 get_ipython().run_line_magic('matplotlib', 'widget')
4
5 self.create_dropdown()
6 self.display_widgets()
7
8 Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
self.mb = mb
self.ai = ai
self.ai_dict = ai_dict
self.size = size
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
self.create_dropdown()
self.display_widgets()
Marubatsu_GUI.__init__ = __init__
修正箇所
def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
元と同じなので省略
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
+ self.create_dropdown()
+ self.display_widgets()
Marubatsu_GUI.__init__ = __init__
上記の修正後に、下記のプログラムを実行して Marubatsu_GUI
クラスのインスタンスを作成すると、Marubatsu_GUI
クラスの __init__
メソッドが実行されるので、実行結果のように Dropdown が表示されます。なお、Marubatsu_GUI
の実引数には、〇×ゲームのインスタンス と 人間 VS 人間 を表すデータを記述しました。また、今夏の記事では利用しませんが、作成した Marubatsu_GUI
クラスのインスタンスを mb_gui
という名前の変数に代入しました。
mb = Marubatsu()
mb_gui = Marubatsu_GUI(mb, ai=[None, None])
実行結果(下図は、画像なので操作することはできません)
ボタンを作成する関数の定義
次に、play
メソッド内の、ボタンを作成するローカル関数 create_button
を Marubatsu_GUI
のメソッドとして定義 します。なお、create_button
は、Marubatsu_GUI
の インスタンスの情報を利用しない ので、以前の記事で説明した @staticmethod
のデコレータを使って 静的メソッド として定義する事にしました。静的メソッドの意味が良くわからない人は、通常のメソッドとして定義 しても 問題はありません。その場合は、@staticmethod
を削除 し、最初の仮引数に self
を追加 して下さい。
@staticmethod
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
style={"button_color": "lightgreen"},
)
Marubatsu_GUI.create_button = create_button
ウィジェットをまとめて作成する関数の定義
次に、create_button
を使って 6 つのボタンを作成する処理を __init__
メソッドに直接記述しても良いのですが、ウィジェットの数が多くなるとわかりづらくなるので、下記のプログラムのように、ウィジェットをまとめて作成 する create_widgets
というメソッドを定義する事にします。先程 __init__
メソッドに記述した Dropdown を作成する処理を記述し、その後で play
メソッドに記述されていた 6 つのボタンを作成する処理を記述します。
特に難しい点はないと思いますので、修正箇所の説明などは省略します。
def create_widgets(self):
# AI を選択する Dropdown を作成する
self.create_dropdown()
# 変更、リセットボタンを作成する
self.change_button = self.create_button("変更", 100)
self.reset_button = self.create_button("リセット", 100)
# リプレイのボタンを作成する
self.first_button = self.create_button("<<", 100)
self.prev_button = self.create_button("<", 100)
self.next_button = self.create_button(">", 100)
self.last_button = self.create_button(">>", 100)
Marubatsu_GUI.create_widgets = create_widgets
ウィジェットが増えたので、ウィジェットを配置して表示する display_widgets
を下記のプログラムのように修正します。play
メソッドの該当する処理の記述との違いは、それぞれのウィジェットが代入されたローカル変数の前に self.
を記述するだけです。
def display_widgets(self):
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
Marubatsu_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
- hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
+ hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
- hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
+ hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
Marubatsu_GUI.display_widgets = display_widgets
次に __init__
メソッドを下記のプログラムのように修正します。
-
2 行目:
create_dropdown
をcreate_widgets
に修正する
1 def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
元と同じなので省略
2 self.create_widgets()
3 self.display_widgets()
4
5 Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
self.mb = mb
self.ai = ai
self.ai_dict = ai_dict
self.size = size
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
self.ai_dict = {}
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
self.create_widgets()
self.display_widgets()
Marubatsu_GUI.__init__ = __init__
修正箇所
def __init__(self, mb, ai, ai_dict=None, params=None, size=3):
元と同じなので省略
- self.create_dropdown()
+ self.create_widgets()
self.display_widgets()
Marubatsu_GUI.__init__ = __init__
上記の修正後に、下記のプログラムを実行して Marubatsu_GUI
クラスのインスタンスを作成すると、実行結果のように Dropdown とボタンが表示されるようになります。なお、ボタンの設定の変更の処理や、イベントハンドラはまだ記述していないので、すべてのボタンの色は緑色で、クリックしても何も行われません。
gui = Marubatsu_GUI(mb, ai=[None, None])
実行結果(下図は、画像なので操作することはできません)
ゲーム盤を表す Figure を作成する関数
次に、ゲーム盤を表す Figure を作成 する create_figure
を下記のプログラムのように定義します。なお、fig
や ax
は、draw_board
メソッド内で ゲーム盤を描画する際などで必要となる ので、Marubatsu_GUI
クラスの属性に代入 しました。
def create_figure(self):
self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
self.fig.canvas.toolbar_visible = False
self.fig.canvas.header_visible = False
self.fig.canvas.footer_visible = False
self.fig.canvas.resizable = False
Marubatsu_GUI.create_figure = create_figure
修正箇所
def create_figure(self):
- fig, ax = plt.subplots(figsize=[size, size])
+ self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
- fig.canvas.toolbar_visible = False
+ self.fig.canvas.toolbar_visible = False
- fig.canvas.toolbar_visible = False
+ self.fig.canvas.toolbar_visible = False
- fig.canvas.footer_visible = False
+ self.fig.canvas.footer_visible = False
- fig.canvas.resizable = False
+ self.fig.canvas.resizable = False
Marubatsu_GUI.create_figure = create_figure
次に、create_widgets
メソッドを下記のプログラムのように修正します。
-
4 行目:
create_figue
を呼び出して、ゲーム盤の画像を表す Figure を作成する
1 def create_widgets(self):
元と同じなので省略
2 self.last_button = self.create_button(">>", 100)
3 # ゲーム盤の画像を表す figure を作成する
4 self.create_figure()
5
6 Marubatsu_GUI.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
# AI を選択する Dropdown を作成する
self.create_dropdown()
# 変更、リセットボタンを作成する
self.change_button = self.create_button("変更", 100)
self.reset_button = self.create_button("リセット", 100)
# リプレイのボタンを作成する
self.first_button = self.create_button("<<", 100)
self.prev_button = self.create_button("<", 100)
self.next_button = self.create_button(">", 100)
self.last_button = self.create_button(">>", 100)
# ゲーム盤の画像を表す figure を作成する
self.create_figure()
Marubatsu_GUI.create_widgets = create_widgets
修正箇所
def create_widgets(self):
元と同じなので省略
self.last_button = self.create_button(">>", 100)
# ゲーム盤の画像を表す figure を作成する
+ self.create_figure()
Marubatsu_GUI.create_widgets = create_widgets
作成した Figure は自動的に VSCode のセルに描画されるので、display_widgets
を修正する必要はありません。上記の修正後に、下記のプログラムを実行して Marubatsu_GUI
クラスのインスタンスを作成すると、実行結果のように Dropdown とボタンの下に Figure が表示されるようになります。ただし、ゲーム盤の画像を描画する処理はまだ記述していないので、ゲーム盤の画像はまだ描画されません。
gui = Marubatsu_GUI(mb, ai=[None, None])
実行結果(下図は、画像なので操作することはできません)
ゲーム盤の描画を行うメソッドの定義は少し長くなるので今回の記事はここまでにします。
今回の記事のまとめ
今回の記事では、AI が手番を担当する場合 の リプレイ機能の問題を修正 しました。
また、GUI に関する処理 を行う Marubatsu_GUI
クラスを定義 し、Marubatsu
クラスの処理の中から GUI の処理を分離 する作業を開始しました。今回の記事では、Marubatsu_GUI
のインスタンスを作成することで、GUI のウィジェットが表示される所まで実装しました。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。なお、Marubatsu_GUI
クラスは、〇×ゲームに関する処理であることには変わりがないので、Marubatsu
クラスの後でその定義を記述することにします。
次回の記事