目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
前回の記事で発生しなかったエラー
前回の記事では、Marubatsu_GUI
クラスを定義 することで Marubatsu
クラスから GUI の機能を分離する作業 の中で、ゲーム盤の描画を行う処理を実装 しました。
実は、前回の記事で記述したプログラムにはバグがあるため、下記のプログラムで gui_play
を実行すると、前回の記事で gui_play
を実行した際に発生しなかった、実行結果のようなエラーが発生します。このエラーの原因について少し考えてみて下さい。
from util import gui_play
gui_play()
実行結果(ボタンやゲーム盤の画像は省略します)
略
File c:\Users\ys\ai\marubatsu\083\marubatsu.py:665, in Marubatsu_GUI.update_widgets_status(self)
662 """ウィジェットの状態を更新する."""
664 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
--> 665 set_button_status(self.first_button, self.mb.move_count <= 0)
666 set_button_status(self.prev_button, self.mb.move_count <= 0)
667 set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
NameError: name 'set_button_status' is not defined
エラーの検証と修正
エラーメッセージから、update_widgets_status
の中で、定義されていない set_button_status
を呼び出そうした ためエラーが発生したことがわかります。
この、set_button_status
は、前回の記事で Marubatsu_GUI
のメソッドとして定義1したものなので、呼び出す際には self.set_button_status
のように記述する必要があります。従って、下記のプログラムのように update_widgets_status
を修正することでこの問題を解決することができます。
-
5 ~ 8 行目:
set_button_status
をself.set_button_status
に修正する
1 from marubatsu import Marubatsu_GUI
2
3 def update_widgets_status(self):
4 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
5 self.set_button_status(self.first_button, self.mb.move_count <= 0)
6 self.set_button_status(self.prev_button, self.mb.move_count <= 0)
7 self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
8 self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)
9
10 Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
from marubatsu import Marubatsu_GUI
def update_widgets_status(self):
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
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.board_records) - 1)
self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)
Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
from marubatsu import Marubatsu_GUI
def update_widgets_status(self):
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
- set_button_status(self.first_button, self.mb.move_count <= 0)
+ self.set_button_status(self.first_button, self.mb.move_count <= 0)
- set_button_status(self.prev_button, self.mb.move_count <= 0)
+ self.set_button_status(self.prev_button, self.mb.move_count <= 0)
- set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
+ self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.board_records) - 1)
- set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)
+ self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.board_records) - 1)
Marubatsu_GUI.update_widgets_status = update_widgets_status
実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play
を実行してエラーが発生しなくなることを確認して下さい。
gui_play()
前回の記事でエラーが発生しなかった理由
前回の記事でこのエラーが発生しなかった点 について疑問に思っている人が多いかもしれません。何故、前回の記事でこのエラーが発生しなかったかについて少し考えてみて下さい。
前回の記事では、下記のプログラムで set_button_status
メソッドを定義しました。
@staticmethod
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
button.disabled = disabled
button.style.button_color = "lightgray" if disabled else "lightgreen"
Marubatsu_GUI.set_button_status = set_button_status
このプログラムでは、通常の関数として set_button_status
を定義した後 で、Marubatsu_GUI
の属性にこの関数を代入 するという形で set_button_status
メソッドを定義しました。そのため、上記のプログラムを実行した後 では、set_button_status
を通常の関数として呼び出すことが可能 です。これが、前回の記事で update_widgets_status
メソッドの中で set_button_status
を呼び出してもエラーが発生しなかった原因です。
一方、今回の記事では、VSCode で新しい JupyterLab のファイルを作成したので、そのファイルでプログラムを実行した際には、上記の set_button_status
という関数は定義されていません。また、marubatsu.py の中で、set_button_status
は、Marubatsu_GUI
クラスの定義の中でメソッドとして定義 されていますが、通常の関数としては定義されていない ので、先程のようなエラーが発生します。
これまでの記事で「関数を定義し、その関数をクラスの属性に代入する」という方法でクラスのメソッドの定義を修正してきたのは、修正するメソッドの定義だけを JupyterLab のセルに記述して実行することで、メソッドを修正できるからです。
この方法は個別のメソッドを修正する際は便利ですが、定義した関数を通常の関数として利用できてしまうという点に注意が必要です。
なお、修正したメソッドを marubatsu.py に反映させる際には、クラスの定義の中に修正したメソッドを記述したほうが良いので、そのようにしています。
draw_mark
の定義の移動
前回の記事では、GUI に関する機能を Marubatsu
クラスから分離するために、ゲーム盤を描画 する処理を行う draw_board
メソッドの定義 を、Marubatsu
クラスから Marubatsu_GUI
クラスのメソッドに移動 しました。筆者は、これで GUI に関する機能を完全に分離できたと勘違いしていたのですが、Marubatsu
クラスの定義をよく見ると、ゲーム盤にマークを描画 する draw_mark
というメソッドが まだ残っている ことに気づきましたので、この関数も Marubatsu_GUI
クラスに移動することにします。
draw_mark
は 静的メソッド として定義したので、前回の記事で 様々なバグの原因となった self
は存在しません。そのため、下記のプログラムのように、単純に その定義をコピーするだけ で Marubatsu_GUI
クラスのメソッドとして定義しなおすことができます。
下記の draw_mark
の定義は Marubatsu
クラスの draw_mark
と全く同じです。
from marubatsu import Marubatsu
import matplotlib.patches as patches
@staticmethod
def draw_mark(ax, x, y, mark, color="black"):
if mark == Marubatsu.CIRCLE:
circle = patches.Circle([x + 0.5, y + 0.5], 0.35, ec=color, fill=False, lw=2)
ax.add_artist(circle)
elif mark == Marubatsu.CROSS:
ax.plot([x + 0.15, x + 0.85], [y + 0.15, y + 0.85], c=color, lw="2")
ax.plot([x + 0.15, x + 0.85], [y + 0.85, y + 0.15], c=color, lw="2")
Marubatsu_GUI.draw_mark = draw_mark
次に、draw_mark
は draw_board
メソッドの中から呼び出されている ので、下記のプログラムのように、draw_board
を修正する必要があります。
-
6 行目:
self.mb.draw_mark
をself.draw_mark
に修正することで、Marubatsu_GUI
クラスのdraw_mark
メソッドを呼び出すように修正する
1 def draw_board(self):
元と同じなので省略
2 # ゲーム盤のマークを描画する
3 for y in range(self.mb.BOARD_SIZE):
4 for x in range(self.mb.BOARD_SIZE):
5 color = "red" if (x, y) == self.mb.last_move else "black"
6 self.draw_mark(ax, x, y, self.mb.board[x][y], color)
7
8 self.update_widgets_status()
9
10 Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
def draw_board(self):
ax = self.ax
ai = self.mb.ai
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていた場合は背景色を
facecolor = "white" if self.mb.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.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
# 引き分けの場合
elif self.mb.status == Marubatsu.DRAW:
text = "Draw game"
# 決着がついていれば勝者を表示する
else:
text = "Winner " + self.mb.status
ax.text(0, -0.2, text, fontsize=20)
# ゲーム盤の枠を描画する
for i in range(1, self.mb.BOARD_SIZE):
ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線
# ゲーム盤のマークを描画する
for y in range(self.mb.BOARD_SIZE):
for x in range(self.mb.BOARD_SIZE):
color = "red" if (x, y) == self.mb.last_move else "black"
self.draw_mark(ax, x, y, self.mb.board[x][y], color)
self.update_widgets_status()
Marubatsu_GUI.draw_board = draw_board
修正箇所
def draw_board(self):
元と同じなので省略
# ゲーム盤のマークを描画する
for y in range(self.mb.BOARD_SIZE):
for x in range(self.mb.BOARD_SIZE):
color = "red" if (x, y) == self.mb.last_move else "black"
- self.mb.draw_mark(ax, x, y, self.mb.board[x][y], color)
+ self.draw_mark(ax, x, y, self.mb.board[x][y], color)
self.update_widgets_status()
Marubatsu_GUI.draw_board = draw_board
実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play
を実行し、正しい処理が行われることを確認して下さい。
gui_play()
イベントハンドラに関する関数の定義
次は、play
メソッドに元々記述されていた、イベントハンドラを定義し、ボタンなどに結びつける処理 を実装します。イベントハンドラも Marubatsu_GUI
クラスのメソッドとして定義すればよいのではないかと思う人がいるかもしれませんが、イベントハンドラ は、Marubatsu_GUI
クラスのインスタンスからではなく、ipywidgets のイベントループの中から呼び出される ので、イベントハンドラに実引数で Marubatsu_GUI
クラスのインスタンスの情報を渡すことはできません。そのため、イベントハンドラの中で Marubatsu_GUI
の情報を利用するためには、play
メソッドで行っていたのと同様に、Marubatsu_GUI
クラスのメソッドの ローカル関数としてイベントハンドラを定義 する必要があります。
create_event_handler
のひな形
そこで、イベントハンドラをローカル関数として定義し、ウィジェットに結びつける処理を行う create_event_handler
というメソッド定義することにします。
前回の記事で、play
メソッドの中から イベントハンドラに関する処理を削除 してしまったので、前々回の記事の play
メソッドの中から、イベントハンドラに関する処理を抜き出してコピー することで、下記のプログラムのように create_event_handler
を定義し、これを雛形に create_event_handler
が正しく動作するように修正 していくことにします。
import math
from copy import deepcopy
def create_event_handler(self):
# 変更ボタンのイベントハンドラを定義する
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)
def change_step(step):
# step の範囲を正しい範囲に修正する
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)
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
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)
Marubatsu_GUI.create_event_handler = create_event_handler
Marubatsu
クラスでの change_step
の定義
create_event_handler
の中で定義されている change_step
が行う処理のうち、ゲーム盤の描画を更新する self.draw_board
以外の処理 は、step
手目の局面に移動するという処理であり、GUI の処理ではありません。また、特定の手数の局面に移動 するという処理は、GUI 以外の場合でも利用できた方が便利 ですが、この関数を create_event_handler
の中の ローカル関数として定義すると GUI でしか利用できない という欠点が生じます。
そこで、change_step
の中の self.draw_board
以外の処理を行うメソッド を 下記のプログラムのように、Marubatsu
クラスに同じ名前と仮引数で定義 して移動することにします。このようにすることで、Marubatsu
クラスのインスタンス から、change_step
を呼び出して、任意の手数の局面に移動できる ようになります。
-
1 行目:ローカル関数をメソッドとして定義し直すので、仮引数
self
を追加する - 4 行目の下にあった描画を更新する処理を削除する
1 def change_step(self, step):
元と同じなので省略
2 # 直前の着手を計算する
3 self.last_move = self.records[step]
4 # この下にあった描画を更新する処理を削除する
5
6 Marubatsu.change_step = change_step
行番号のないプログラム
def change_step(self, step):
# step の範囲を正しい範囲に修正する
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]
Marubatsu.change_step = change_step
修正箇所
-def change_step(step):
+def change_step(self, step):
# step の範囲を正しい範囲に修正する
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)
Marubatsu.change_step = change_step
次に、create_event_handler
のローカル関数 change_step
を下記のプログラムのように修正します。
-
3 行目:手数を移動する処理を削除し、代わりに先程定義した
Marubatsu
クラスのchange_step
を呼び出す - 5 行目:描画を更新する処理は GUI の処理なので元のまま残す
1 def create_event_handler(self):
元と同じなので省略
2 def change_step(step):
3 self.mb.change_step(step)
4 # 描画を更新する
5 self.draw_board(ax, ai)
元と同じなので省略
6
7 Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
# 変更ボタンのイベントハンドラを定義する
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)
def change_step(step):
self.mb.change_step(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)
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
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)
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
def change_step(step):
# step の範囲を正しい範囲に修正する
- 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.mb.change_step(step)
# 描画を更新する
self.draw_board(ax, ai)
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
なお、このローカル関数 change_step
を Marubatsu_GUI
クラスのメソッドとして定義する事もできますが、本記事では以下の理由からローカル関数として定義する事にします。
- ローカル関数
change_step
は、create_event_handler
の中で定義されたイベントハンドラの中からしか呼び出されない -
change_step
をMarubatsu_GUI
のメソッドとして定義すると、self.change_step
と記述した場合 に、Marubatsu
クラスとMarubatsu_GUI
クラスの どちらのchange_step
メソッドを呼び出しているかがわかりにくくなる。なお、この問題を解決する別の方法として、change_step
とは別の名前を付けるという方法も考えられる
create_event_handler
のブロックの中の名前の修正
上記の create_event_handler
は、VSCode では下図のように多くの部分に、名前が定義されていないことを表すオレンジ色の波線が表示 されます。
これらの波線は、前回の記事で、play
メソッドの中でローカル変数だったものが、Marubatsu_GUI
の属性に変更されたことなどが原因で 定義されなくなった名前 を表します。
また、play_loop
などの 仮引数が変更されたメソッド などや、前回の記事で多くのバグの原因となった self
の意味の混同に関する修正 を行う必要があります。
下記は修正を行う必要がある名前の一覧です。本記事では、これらをすべて修正したプログラムを下記に示しますが、このような複雑で大量の修正を行う必要がある場合は、修正漏れが発生し、プログラムを実行するとエラーが発生する可能性が高い でしょう。そのような場合は、前回の記事で行ったように、エラーが発生するたびに原因を検証して修正するという、地道な作業を行う 必要があります。
Marubatsu
クラスの属性になった名前
下記は、先頭に self.mb.
をつけるという修正を行う必要があります。
行数 | |
---|---|
ai |
5 |
self.play_loop |
6、48 |
self.restart |
10 |
move_count |
26、29 |
board_records |
32 |
status |
42 |
move |
45 |
Marubatsu_GUI
クラスの属性になった名前
下記は、先頭に self.
をつけるという修正を行う必要があります。
行数 | |
---|---|
dropdown_list |
5 |
change_button |
14 |
reset_button |
15 |
fig |
51 |
- 仮引数が変化したメソッド
変更点 | 行数 | |
---|---|---|
play_loop |
仮引数が無くなった | 6、48 |
draw_board |
仮引数が無くなった | 20、46 |
変更してはいけない名前
下記の名前は Marubatsu_GUI
の属性なので、変更してはいけない点に注意して下さい。
* self.draw_board
* self.first_button
* self.prev_button
* self.next_button
* self.last_button
なお、上記の修正は、下記の理由から前回の記事で紹介したシンボル名の変更の機能を利用することはできないので、検索や置換の機能を使って地道に行う必要があります。
-
ai
などは、名前が定義されていないので 文法的に正しくない -
異なる意味の
self
が混在する ので、シンボル名の変更を行うと、変更してはいけないself
までもが変更されてしまう
修正した create_event_handler
の定義
下記は、上記の修正を行った create_event_handler
の定義を行うプログラムです。
1 def create_event_handler(self):
2 # 変更ボタンのイベントハンドラを定義する
3 def on_change_button_clicked(b):
4 for i in range(2):
5 self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
6 self.mb.play_loop()
7
8 # リセットボタンのイベントハンドラを定義する
9 def on_reset_button_clicked(b):
10 self.mb.restart()
11 on_change_button_clicked(b)
12
13 # イベントハンドラをボタンに結びつける
14 self.change_button.on_click(on_change_button_clicked)
15 self.reset_button.on_click(on_reset_button_clicked)
16
17 def change_step(step):
18 self.mb.change_step(step)
19 # 描画を更新する
20 self.draw_board()
21
22 def on_first_button_clicked(b):
23 change_step(0)
24
25 def on_prev_button_clicked(b):
26 change_step(self.mb.move_count - 1)
27
28 def on_next_button_clicked(b):
29 change_step(self.mb.move_count + 1)
30
31 def on_last_button_clicked(b):
32 change_step(len(self.mb.board_records) - 1)
33
34 self.first_button.on_click(on_first_button_clicked)
35 self.prev_button.on_click(on_prev_button_clicked)
36 self.next_button.on_click(on_next_button_clicked)
37 self.last_button.on_click(on_last_button_clicked)
38
39 # ゲーム盤の上でマウスを押した場合のイベントハンドラ
40 def on_mouse_down(event):
41 # Axes の上でマウスを押していた場合のみ処理を行う
42 if event.inaxes and self.mb.status == Marubatsu.PLAYING:
43 x = math.floor(event.xdata)
44 y = math.floor(event.ydata)
45 self.mb.move(x, y)
46 self.draw_board()
47 # 次の手番の処理を行うメソッドを呼び出す
48 self.mb.play_loop()
49
50 # fig の画像にマウスを押した際のイベントハンドラを結び付ける
51 self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)
52
53 Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
self.mb.play_loop()
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.mb.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
self.change_button.on_click(on_change_button_clicked)
self.reset_button.on_click(on_reset_button_clicked)
def change_step(step):
self.mb.change_step(step)
# 描画を更新する
self.draw_board()
def on_first_button_clicked(b):
change_step(0)
def on_prev_button_clicked(b):
change_step(self.mb.move_count - 1)
def on_next_button_clicked(b):
change_step(self.mb.move_count + 1)
def on_last_button_clicked(b):
change_step(len(self.mb.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)
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
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)
self.mb.move(x, y)
self.draw_board()
# 次の手番の処理を行うメソッドを呼び出す
self.mb.play_loop()
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
# 変更ボタンのイベントハンドラを定義する
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.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
- self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
+ self.mb.play_loop()
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
- self.restart()
+ self.mb.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
- change_button.on_click(on_change_button_clicked)
+ self.change_button.on_click(on_change_button_clicked)
- reset_button.on_click(on_reset_button_clicked)
+ self.reset_button.on_click(on_reset_button_clicked)
def change_step(step):
self.mb.change_step(step)
# 描画を更新する
- self.draw_board(ai, ax)
+ self.draw_board()
def on_first_button_clicked(b):
change_step(0)
def on_prev_button_clicked(b):
- change_step(self.move_count - 1)
+ change_step(self.mb.move_count - 1)
def on_next_button_clicked(b):
- change_step(self.move_count + 1)
+ change_step(self.mb.move_count + 1)
def on_last_button_clicked(b):
- change_step(len(self.board_records) - 1)
+ change_step(len(self.mb.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)
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
- if event.inaxes and self.status == Marubatsu.PLAYING:
+ if event.inaxes and self.mb.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
- self.move(x, y)
+ self.mb.move(x, y)
- self.draw_board(ai, ax)
+ self.draw_board()
# 次の手番の処理を行うメソッドを呼び出す
- self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
+ self.mb.play_loop()
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
- fig.canvas.mpl_connect("button_press_event", on_mouse_down)
+ self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)
Marubatsu_GUI.create_event_handler = create_event_handler
このように、バグの修正はかなり地道な作業が必要になりますが、避けて通ることはできません。個人的には、プログラミングで重要となる能力の一つ が、何度も何度も発生するバグを修正する 忍耐力 ではないかと思っています。
次に、下記のプログラムの 3 行目のように、Marubatsu_GUI
の __init__
内で、create_event_handler
を呼び出すように修正します。
1 def __init__(self, mb, ai_dict=None, size=3):
元と同じなので省略
2 self.create_widgets()
3 self.create_event_handler()
4 self.display_widgets()
5
6 Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
def __init__(self, mb, ai_dict=None, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
self.mb = mb
self.ai_dict = ai_dict
self.size = size
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
self.create_widgets()
self.create_event_handler()
self.display_widgets()
Marubatsu_GUI.__init__ = __init__
修正箇所
def __init__(self, mb, ai_dict=None, size=3):
元と同じなので省略
self.create_widgets()
self.create_event_handler()
self.display_widgets()
Marubatsu_GUI.__init__ = __init__
実行結果は省略しますが、上記の修正後に、下記のプログラムで、gui_play
を実行すると、ゲーム盤のクリックによる着手や、リセットボタンやリプレイボタンなどが正しく動作するようになることが確認できます。
gui_play()
AI が手番を担当した場合のエラーと修正
上記では、人間どうしが対戦した場合の確認を行いましたが、Dropdown で AI を選択して AI どうしが対戦をおこなうようにしてからリセットボタンをクリックすると、下記のようなエラーが発生します。このエラーに関しては、これまでの知識では理解できないと思います。
略
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\site-packages\ipywidgets\widgets\widget.py:512, in Widget.__deepcopy__(self, memo)
511 def __deepcopy__(self, memo):
--> 512 raise NotImplementedError("Widgets cannot be copied; custom implementation required")
NotImplementedError: Widgets cannot be copied; custom implementation required
上記のエラーメッセージは、以下のような意味を持ちます。
-
NotImplementedError
実装(Implemented)されていない(Not)ことを表すエラー -
Widgets cannot be copied; custom implementation required
ウィジェット(Widgets)はコピーできない(can not be copied)。(ウィジェットをコピーするためには)独自の(custon)実装(implementation)が必要である(required)
エラーの原因の検証
エラーメッセージから、ウィジェットをコピーしようとして発生したエラー であることがわかります。そこで、どこでウィジェットがコピーされたかを検証 することにします。
このエラーは、人間どうしの対戦では発生しなかったので、AI が着手を選択する処理の中で発生している可能性が高い と推測できます。先ほどの エラーメッセージをさかのぼってみてみる と、下記のように、AI が着手を選択する際に実行する ai_by_score
の中で、deepcopy(mb_orig)
を呼び出した際にこのエラーが発生する ことがわかります。
File c:\Users\ys\ai\marubatsu\083\ai.py:130, in ai_by_score(mb_orig, eval_func, debug, rand)
128 dprint(debug, "=" * 20)
129 dprint(debug, "move", move)
--> 130 mb = deepcopy(mb_orig)
131 x, y = move
132 mb.move(x, y)
deepcopy
は以前の記事で説明したように、深いコピー を行う組み込み関数です。深いコピーは、オブジェクトから辿ることができるすべての属性をコピーする処理 が行われるので、この部分では下記のような処理が行われます。
-
mb_orig
はMarubatsu
クラスのインスタンスなので、Marubatsu
クラスのインスタンスの深いコピー の処理が行われる -
deepcopy
は、mg_orig
の すべての属性をコピー するという処理を行う -
Marubatsu
クラスのインスタンスには、mb_gui
という、Marubatsu_GUI
クラスのインスタンスが代入された属性 が存在する -
deepcopy
はmb_gui
属性をコピー する際に、mb_gui
に代入されたMarubatsu_GUI
クラスのインスタンスの すべての属性をコピー するという処理を行う -
mb_gui
属性 には、first_button
など の、ウィジェットが代入された属性が存在する
上記から、deepcopy(mb_orig)
によって、ウィジェットをコピーする処理が行われる ことが確認できました。
エラーの修正
これまでの記事では説明していませんでしたが、deepcopy
には、ウィジェットなど、コピーすることができないオブジェクト があり、そのようなオブジェクトをコピーしようとすると、先程のような NotImplementedError
というエラーが発生 します。従って、このエラーを修正するためには、Marubatsu
クラスのインスタンスから辿れるの属性の中 に、ウィジェットのデータが存在しないようにする 必要があります。
ウィジェットのデータが代入 されているのは、Marubatsu_GUI
クラスのインスタンスが代入されている mb_gui
属性 です。Marubatsu_GUI
クラス は 〇×ゲームを GUI で遊ぶために必要な処理を行う ものですが、AI が着手を選択する際に GUI の機能は必要がありません。そのため、Marubatsu
クラスのインスタンスに mb_gui
属性が存在しなくても AI が着手を選択することができます。そこで、Marubatsu
クラスのインスタンスから mg_gui
属性を削除する ことで、上記の問題を解決することにします。
具体的には、下記のようにプログラムを修正します。
-
play
メソッド内で作成するMarubatsu_GUI
クラスのインスタンスを ローカル変数mb_gui
に代入 する -
play_loop
内 のself.mb_gui
が利用できなくなる ので、その代わりにMarubatsu_GUI
クラスのインスタンスを代入する 仮引数mb_gui
をplay_loop
に追加する -
play_loop
を呼び出す処理の 実引数にMarubatsu_GUI
クラスのインスタンスを記述 する
deepcopy
がコピーするデータは 、オブジェクトから辿れる 属性の値だけ で、ローカル変数の値はコピーしません。従って、上記の修正を行うことで、deepcopy
がウィジェットのデータをコピーしなくなるため、エラーが発生しなくなります。
下記は、play
メソッドを修正したプログラムです。
-
4 行目:
Marubatsu_GUI
のインスタンスをローカル変数mb_gui
に代入するようにする -
7 行目:
play_loop
の実引数にmb_gui
を追加する
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
3 if gui:
4 mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)
5
6 self.restart()
7 return self.play_loop(mb_gui)
8
9 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# 一部の仮引数をインスタンスの属性に代入する
self.ai = ai
self.params = params
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, ai_dict=ai_dict, size=size)
self.restart()
return self.play_loop(mb_gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
- self.mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)
+ mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)
self.restart()
- return self.play_loop()
+ return self.play_loop(mb_gui)
Marubatsu.play = play
上記の 4 行目の修正は、前回の記事で行った修正を元に戻す という修正でなので、前回の記事の修正は無駄だったのではないか と思った人がいるかもしれません。
確かに、結果として は前回の記事の修正は 無駄のように見えるかもしれません が、前回の記事の時点 では 意味のある修正 であり、今回の記事で前回とは 事情が変化したため 元に戻したので、一概には無駄であると言えません。
プログラムでは、それほど頻繁ではありませんが、一度何らかの理由で修正した内容 を、後から別の理由で元に戻す ということが実際行われることがあります。
下記は、play_loop
メソッドを修正したプログラムです。
-
1 行目:仮引数
mb_gui
を追加する -
5、9 行目:
self.mb_gui
をmb_gui
に修正する
1 def play_loop(self, mb_gui):
元と同じなので省略
2 if gui:
3 # AI どうしの対戦の場合は画面を描画しない
4 if ai[0] is None or ai[1] is None:
5 mb_gui.draw_board()
元と同じなので省略
6 # 決着がついたので、ゲーム盤を表示する
7 if verbose:
8 if gui:
9 mb_gui.draw_board()
10 else:
11 print(self)
12
13 return self.status
14
15 Marubatsu.play_loop = play_loop
行番号のないプログラム
def play_loop(self, mb_gui):
ai = self.ai
params = self.params
verbose = self.verbose
gui = self.gui
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲーム盤の表示
if verbose:
if gui:
# AI どうしの対戦の場合は画面を描画しない
if ai[0] is None or ai[1] is None:
mb_gui.draw_board()
# 手番を人間が担当する場合は、play メソッドを終了する
if ai[index] is None:
return
else:
print(self)
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
x, y = ai[index](self, **params[index])
else:
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
# "exit" が入力されていればメッセージを表示して関数を終了する
if coord == "exit":
print("ゲームを終了します")
return
# x 座標と y 座標を要素として持つ list を計算する
xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
if len(xylist) != 2:
# エラーメッセージを表示する
print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
continue
x, y = xylist
# (x, y) に着手を行う
try:
self.move(int(x), int(y))
except:
print("整数の座標を入力して下さい")
# 決着がついたので、ゲーム盤を表示する
if verbose:
if gui:
mb_gui.draw_board()
else:
print(self)
return self.status
Marubatsu.play_loop = play_loop
修正箇所
-def play_loop(self):
+def play_loop(self, mb_gui):
元と同じなので省略
if gui:
# AI どうしの対戦の場合は画面を描画しない
if ai[0] is None or ai[1] is None:
- self.mb_gui.draw_board()
+ mb_gui.draw_board()
元と同じなので省略
# 決着がついたので、ゲーム盤を表示する
if verbose:
if gui:
- self.mb_gui.draw_board()
+ mb_gui.draw_board()
else:
print(self)
return self.status
Marubatsu.play_loop = play_loop
play_loop
は create_event_handler
内で呼び出されているので、その部分を下記のプログラムのように修正します。
-
6、16 行目:
play_loop
の実引数に、Marubatsu_GUI
のインスタンスを表すself
を追加する。紛らわくてわかりづらいが、play_loop
の仮引数self
にはMarubatsu
クラスのインスタンスであるself.mb
が、play_loop
の仮引数mb_gui
には、Marubatsu_GUI
クラスのインスタンスであるself
が代入される
1 def create_event_handler(self):
元と同じなので省略
2 # 変更ボタンのイベントハンドラを定義する
3 def on_change_button_clicked(b):
4 for i in range(2):
5 self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
6 self.mb.play_loop(self)
元と同じなので省略
7 # ゲーム盤の上でマウスを押した場合のイベントハンドラ
8 def on_mouse_down(event):
9 # Axes の上でマウスを押していた場合のみ処理を行う
10 if event.inaxes and self.mb.status == Marubatsu.PLAYING:
11 x = math.floor(event.xdata)
12 y = math.floor(event.ydata)
13 self.mb.move(x, y)
14 self.draw_board()
15 # 次の手番の処理を行うメソッドを呼び出す
16 self.mb.play_loop(self)
元と同じなので省略
17
18 Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
self.mb.play_loop(self)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.mb.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
self.change_button.on_click(on_change_button_clicked)
self.reset_button.on_click(on_reset_button_clicked)
def change_step(step):
self.mb.change_step(step)
# 描画を更新する
self.draw_board()
def on_first_button_clicked(b):
change_step(0)
def on_prev_button_clicked(b):
change_step(self.mb.move_count - 1)
def on_next_button_clicked(b):
change_step(self.mb.move_count + 1)
def on_last_button_clicked(b):
change_step(len(self.mb.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)
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
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)
self.mb.move(x, y)
self.draw_board()
# 次の手番の処理を行うメソッドを呼び出す
self.mb.play_loop(self)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
- self.mb.play_loop()
+ self.mb.play_loop(self)
元と同じなので省略
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
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)
self.mb.move(x, y)
self.draw_board()
# 次の手番の処理を行うメソッドを呼び出す
- self.mb.play_loop()
+ self.mb.play_loop(self)
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
実行結果は省略しますが、上記の修正後に、下記のプログラムで、gui_play
を実行すると、手番を AI が担当しても、ゲーム盤のクリックによる着手や、リセットボタンやリプレイボタンなどが正しく動作するようになることが確認できます。
gui_play()
CUI での動作確認
GUI の機能を実装するためにさまざまなプログラムの変更を行ってきたので、CUI でプログラムが正しく動作するか を下記のプログラムで確認することにします。実行結果から、エラーが発生することがわかります。このエラーの原因について少し考えてみて下さい。
from ai import ai1s
mb = Marubatsu()
mb.play(ai=[ai1s, ai1s])
実行結果
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
Cell In[17], line 4
1 from ai import ai1s
2 mb = Marubatsu()
----> 4 mb.play(ai=[ai1s, ai1s])
Cell In[13], line 17
14 mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)
16 self.restart()
---> 17 return self.play_loop(mb_gui)
UnboundLocalError: cannot access local variable 'mb_gui' where it is not associated with a value
エラーの検証と修正
エラーメッセージから、play
メソッド内で self.play_loop(mb_gui)
を呼び出す際 に、mb_gui
に値が代入されていない ことがわかります。play
メソッドの中で mb_gui
は gui
が True
の場合だけ値が代入される ので、gui
が False
の場合 に何らかの 値を代入する必要があります。どのような値を入力すればよいかについて少し考えてみて下さい。
mb_gui
は play_loop
の中で、gui
が True
の場合のみ利用されます。従って、gui
が False
の場合 は、mb_gui
には何が代入されていてもかまわない ことがわかります。
そこで、本記事では、下記のプログラムの 5、6 行目のようには、gui
が False
の場合は mb_gui
に None
を代入する ことにします。
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
3 if gui:
4 mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)
5 else:
6 mb_gui = None
元と同じなので省略
7
8 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# 一部の仮引数をインスタンスの属性に代入する
self.ai = ai
self.params = params
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, ai_dict=ai_dict, size=size)
else:
mb_gui = None
self.restart()
return self.play_loop(mb_gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
mb_gui = Marubatsu_GUI(self, ai_dict=ai_dict, size=size)
else:
mb_gui = None
元と同じなので省略
Marubatsu.play = play
上記のプログラムが行う処理は、GUI の機能を分離する前の play
メソッドで、gui
が False
の場合に ax
に None
を代入していたのと同様です。
実行結果は省略しますが、下記のプログラムを実行することで、CUI でも正しくプログラムが動作することが確認できます。下記は AI どうしの対戦ですが、人間を含めた CUI の対戦でも正しくプログラムが動作することを確認してみて下さい。
mb.play(ai=[ai1s, ai1s])
これでリプレイのために最低限必要な機能は実装できましたが、まだいくつか改良できる部分があるので、その方法を紹介したいと思います。
着手の記録による局面の再現
これまでは、ゲーム盤のデータを記録する方法 として、以前の記事で紹介した、board_records
属性に代入された list の要素 に、それぞれの手数の局面を表すデータを代入する という方法を採用してきました。この方法は プログラムの記述が簡単に行える という利点がある一方で、board_records
属性に 最大で 0 手目から 9 手目までの、10 個の局面のデータを保存する必要がある という欠点があります。
Marubatsu
クラスのインスタンスの 属性に記録するデータの量が増える と、その分だけ AI が着手を行う際に実行する deepcopy
の処理に時間がかかってしまう ため、ゲーム盤のデータを記録する 前と後 では、AI が対戦したときにかかる時間が増えてしまいます。
そのことを示すために、下記のプログラムで ai1s
どうし(他の AI でもかまいません)が 10000 回対戦した時にかかる時間を計測 してみることにします。実行結果は重要ではないので省略しますが、筆者のパソコンで下記のプログラムを実行すると 約 67 秒 かかりました。
from ai import ai_match
ai_match(ai=[ai1s, ai1s])
着手の記録を使った局面の再現の仕組み
Marubatsu
クラスのインスタンスには、各手数で行われた 着手を記録する records
属性 が存在しますが、このデータを利用 すれば、board_records
属性のデータが存在しなくても 、好きな手数の局面を再現する ことができます。その方法について少し考えてみて下さい。
例えば、3 手目の局面は以下の手順で再現できます。
- ゲーム盤をゲーム開始時の状態にする
-
records
属性から 3 手分の着手のデータを順番に取り出し、着手を行う
この方針に従って、任意の手数の局面に移動する Marubatsu
クラスの change_step
メソッドを修正する方法について少し考えてみて下さい。
各手数で行われた着手のデータは、前の局面と次の局面の差異を表すデータ なので、このようなデータのことを 差分データ と呼びます。
〇×ゲームに限らず、オセロ、将棋、囲碁などのデータは、差分を表す着手のデータがあれば、任意の局面のデータを再現できる ので、すべての局面のデータを記録するかわりに、着手のデータだけを記録 することで、記録する必要があるデータの量を大幅に削減する ことが良く行われます。
例えば、将棋のゲーム盤は 81 マスあるので、将棋の各局面のデータを記録するためにはかなりの量のデータが必要なりますが、着手のデータは局面のデータと比較すると数十分の一以下の小さなデータで表現できます。また、将棋はゲームが終了するまで平均すると約 100 回の着手が行われるので、着手のデータだけを記録することで、記録するデータを実際に大幅に削減することができます。
余談ですが、動画は画像を高速にパラパラ漫画のように切り替えて表示するという仕組みになっています。それぞれの画像の内容はほとんど変わらないので、すべての画像をそのまま記録する代わりに、前の画像と次の画像の差分のデータだけを記録することで、動画のデータを大幅に小さくするという工夫が行われています。
board_records
属性の削除
まず、不要になった board_records
属性を削除する ことから始めることにします。board_records
属性は多くの場所で記述されているので少々面倒ですが、VSCode の Ctrl + F による検索機能 を使って、board_records
を検索すると良い でしょう。
下記は、restart
メソッドから board_records
を削除したプログラムです。
def restart(self):
元と同じなので省略
self.records = [self.last_turn]
# この下にあった board_records 属性を初期化する処理を削除する
Marubatsu.restart = restart
行番号のないプログラム
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
self.records = [self.last_turn]
Marubatsu.restart = restart
修正箇所
def restart(self):
元と同じなので省略
self.records = [self.last_turn]
- self.board_records = [deepcopy(self.board)]
Marubatsu.restart = restart
下記は、move
メソッドから board_records
を削除したプログラムです。なお、board_records
属性を削除することで、元のプログラムの len(self.board_records)
を修正 する必要があります。board_records
の要素の数は、records
属性の要素の数と同じ なので、len(self.records)
に修正します。
-
3 行目:
len(self.board_records)
をlen(self.records)
に修正する - 4、7、10 行目の下にあった
board_records
属性に関する処理を削除する
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
元と同じなので省略
3 if len(self.records) <= self.move_count:
4 # この下にあった board_records 属性に関する処理を削除する
5 self.records.append(self.last_move)
6 else:
7 # この下にあった board_records 属性に関する処理を削除する
8 self.records[self.move_count] = self.last_move
9 self.records = self.records[0:self.move_count + 1]
10 # この下にあった board_records 属性に関する処理を削除する
11
12 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
if len(self.records) <= self.move_count:
self.records.append(self.last_move)
else:
self.records[self.move_count] = self.last_move
self.records = self.records[0:self.move_count + 1]
Marubatsu.move = move
修正箇所
def move(self, x, y):
if self.place_mark(x, y, self.turn):
元と同じなので省略
- if len(self.board_records) <= self.move_count:
+ if len(self.records) <= self.move_count:
- self.board_records.append(deepcopy(self.board))
self.records.append(self.last_move)
else:
- self.board_records[self.move_count] = deepcopy(self.board)
self.records[self.move_count] = self.last_move
self.records = self.records[0:self.move_count + 1]
- self.board_records = self.board_records[0:self.move_count + 1]
Marubatsu.move = move
下記は、上記と同様に update_widgets_status
メソッドの len(self.board_records)
を修正 したプログラムです。
-
5、6 行目:
len(self.board_records)
をlen(self.records)
に修正する
1 def update_widgets_status(self):
2 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
3 self.set_button_status(self.first_button, self.mb.move_count <= 0)
4 self.set_button_status(self.prev_button, self.mb.move_count <= 0)
5 self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
6 self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)
7
8 Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
def update_widgets_status(self):
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
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)
Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
def update_widgets_status(self):
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
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.board_records) - 1)
+ 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.board_records) - 1)
+ self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)
Marubatsu_GUI.update_widgets_status = update_widgets_status
下記は、上記と同様に create_event_handler
メソッドの len(self.board_records)
を修正 したプログラムです。
-
3 行目:
len(self.board_records)
をlen(self.records)
に修正する
1 def create_event_handler(self):
元と同じなので省略
2 def on_last_button_clicked(b):
3 change_step(len(self.mb.records) - 1)
元と同じなので省略
4
5 Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
self.mb.play_loop(self)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.mb.restart()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
self.change_button.on_click(on_change_button_clicked)
self.reset_button.on_click(on_reset_button_clicked)
def change_step(step):
self.mb.change_step(step)
# 描画を更新する
self.draw_board()
def on_first_button_clicked(b):
change_step(0)
def on_prev_button_clicked(b):
change_step(self.mb.move_count - 1)
def on_next_button_clicked(b):
change_step(self.mb.move_count + 1)
def on_last_button_clicked(b):
change_step(len(self.mb.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)
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
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)
self.mb.move(x, y)
self.draw_board()
# 次の手番の処理を行うメソッドを呼び出す
self.mb.play_loop(self)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
def on_last_button_clicked(b):
- change_step(len(self.mb.board_records) - 1)
+ change_step(len(self.mb.records) - 1)
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
change_step
メソッドについては、この後でまとめて修正することにします。
change_step
メソッドの修正方法
change_step
の修正を、下記のプログラムのように行えば良いと思ったはいないでしょうか?この発想は悪くないと思いますが、実際にはこの修正ではうまくいきません。
-
3 行目:
len(self.board_records)
をlen(self.records)
に修正する - 4 行目:ゲームをリセットして、ゲームの開始時の局面にする
-
5、6 行目:
records
属性から 1 ~ step 手目の着手 を繰り返し処理を使って 順番に取り出し、move
メソッドで着手を行う。records
属性の 0 番の要素 にはNone
が代入 されているので、1 番の要素から取り出す必要がある 点に注意する事
なお、元のプログラムで行っていた、board
や move_count
などを更新する処理 は move
メソッド内で行われる ので 記述する必要はありません。
1 def change_step(self, step):
2 # step の範囲を正しい範囲に修正する
3 step = max(0, min(len(self.records) - 1, step))
4 self.restart()
5 for x, y in self.records[1:step+1]:
6 self.move(x, y)
7
8 Marubatsu.change_step = change_step
行番号のないプログラム
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
self.restart()
for x, y in self.records[1:step+1]:
self.move(x, y)
Marubatsu.change_step = change_step
修正箇所
def change_step(self, step):
# step の範囲を正しい範囲に修正する
- step = max(0, min(len(self.board_records) - 1, step))
+ step = max(0, min(len(self.records) - 1, step))
+ self.restart()
+ for x, y in self.records[1:step+1]:
+ self.move(x, y)
# 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]
Marubatsu.change_step = change_step
上記の修正後に、下記のプログラムで gui_play
を実行し、いくつかの着手を行った後でリプレイ機能のボタンをクリック すると、どのような場合 でも実行結果のように、ゲーム開始時の局面が表示 され、すべてのリプレイ機能のボタンが灰色で表示 されて操作できなくなります。このような現象が起きる原因について少し考えてみて下さい。
gui_play()
実行結果(下図は、画像なので操作することはできません)
問題の原因の検証
この問題の原因は、change_step
の中の self.restart()
にあります。restart
メソッドは、下記のプログラムの 3 行目のように、ゲーム盤をゲーム開始時の状態にするだけではなく、records
属性の値も [self.last_turn]
で初期化してしまう からです。
1 def restart(self):
略
2 self.last_turn = None
3 self.records = [self.last_turn]
従って、下記の change_step
は、仮引数 step
にどのような値が代入されていた場合でも、下記のような処理が行われ、画面にはゲーム開始時の局面が表示されるようになります。
-
self.last_turn
には、上記のrestart
の 2 行目でNone
が代入される ので、self.restart()
を実行後のself.records
には必ず[None]
が代入される -
for x, y in self.records[1:step+1]:
で 1 ~ step 番の要素 を取り出そうとしても、self.records
には 0 番の要素しか代入されておらず、そのような要素は存在しない ため、繰り返し処理のブロック は 一度も実行されない - 結果として、
change_step
では、self.restart()
によってゲームが初期化された後に 何の処理も行われない ので、ゲーム開始時の局面の状態になる
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
self.restart()
for x, y in self.records[1:step+1]:
self.move(x, y)
問題の修正
この問題は、restart
メソッドを実行すると、records
属性の値が別の値で置き換わってしまう ことが原因です。従って、下記のプログラムのように、restart
メソッドを実行する前 に、records
属性の値を別の変数に代入して取っておく という方法が考えられます。
-
6 行目:
self.records
の値をローカル変数records
に代入して取っておく -
8 行目:取っておいた
records
を使って繰り返し処理を行うように修正する
1 def change_step(self, step):
2 # step の範囲を正しい範囲に修正する
3 step = max(0, min(len(self.records) - 1, step))
4 # self.records の値は restart メソッドを呼び出すと初期化されるので
5 # ローカル変数 records に代入して取っておく
6 records = self.records
7 self.restart()
8 for x, y in records[1:step+1]:
8 self.move(x, y)
9
10 Marubatsu.change_step = change_step
行番号のないプログラム
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
# self.records の値は restart メソッドを呼び出すと初期化されるので
# ローカル変数 records に代入して取っておく
records = self.records
self.restart()
for x, y in records[1:step+1]:
self.move(x, y)
Marubatsu.change_step = change_step
修正箇所
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
# self.records の値は restart メソッドを呼び出すと初期化されるので
# ローカル変数 records に代入して取っておく
+ records = self.records
self.restart()
- for x, y in self.records[1:step+1]:
+ for x, y in records[1:step+1]:
self.move(x, y)
Marubatsu.change_step = change_step
ところで、上記のプログラムの 4 行目で、records = deepcopy(self.records)
のように、self.records
をコピーする必要があるのではないかと思った人はいないでしょうか?
上記の場合は コピーを行う必要はありません。その理由は、restart
メソッドの中 で、self.records
に 別の新しい list を代入 するという処理を行っているからです。
self.records
に 別の値を代入 すると、records
と self.records
の 共有が解除 され、別々の list のデータが代入 されることになります。そのため、restart
メソッドを実行しても、records
の値が変化することはありません。また、この後で move
メソッドを実行して self.records
の要素が変化しても、records
の値が変化することはありません。
records = deepcopy(self.records)
でデータをコピーしてもプログラムは正しく動作しますが、copy
や deepcopy
による データのコピー は、代入による データの共有の処理より も、大幅に処理に時間がかかる ので、必要がなければ deepcopy
や copy
は利用しないほうが良いでしょう。
上記の修正後に gui_play
を実行し、いくつかの着手を行った後でリプレイ機能の << や < ボタンをクリックすると、正しい局面が表示されるようになります。しかし、> と >> ボタンが灰色で表示されて操作できないという問題が発生します。
例えば、3 手目の局面で < をクリックすると、実行結果のような画面が表示されます。このような現象が起きる原因について少し考えてみて下さい。
gui_play()
実行結果(下図は、画像なので操作することはできません)
新たな問題の検証
> と >> ボタンが灰色で表示されるということは、その局面が最後に着手が行われた局面 であることを意味します。そのことを念頭において、change_step
で行われる処理を検証 することにします。下記の change_step
では、下記のような処理が行われます。
- ローカル変数
records
にself.records
が代入される -
self.restart()
でゲームがリセットされる - 1 ~ step 手目の着手が
records
から順番に取り出され、move
メソッドで着手が行われる
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
# self.records の値は restart メソッドを呼び出すと初期化されるので
# ローカル変数 records に代入して取っておく
records = self.records
self.restart()
for x, y in records[1:step+1]:
self.move(x, y)
move
メソッドの処理では、records
属性に、行われた 着手のデータが追加 されます。change_step
ではゲーム 開始時の局面から step
回の着手の処理が行われる ので、records
の要素には 0 ~ step
手目の着手のデータ が代入されます。従って、step
手目が最後に行われた着手 になるため、> と >> ボタンが灰色で表示されることになります。
この問題は、下記のプログラムの 8 行目のように self.records
に、ゲームをリセットする前に取っておいた records
を代入しなおして着手の記録を元に戻す ことで解決できます。
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
9
10 Marubatsu.change_step = change_step
行番号のないプログラム
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
records = self.records
self.restart()
for x, y in records[1:step+1]:
self.move(x, y)
self.records = records
Marubatsu.change_step = change_step
修正箇所
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
records = self.records
self.restart()
for x, y in records[1:step+1]:
self.move(x, y)
+ self.records = records
Marubatsu.change_step = change_step
上記の修正後に下記のプログラムを実行して ai1s
どうしの対戦を 10000 回行うことで、処理時間が短縮されたことを確認することにします。
ai_match(ai=[ai1s, ai1s])
筆者のパソコンでは上記のプログラムは 約 45 秒 かかりました。board_records
属性を削除する前は 約 67 秒 だったので、board_records
属性を削除 することで、AI の処理の時間が約 2/3 になり、実際にかなり短縮された ことを確認することができました。
なお、処理時間が短縮された理由は、AI が着手を選択する際に行う deepcopy
の処理の時間が短くなっただけではありません。Marubatsu
クラスから board_records
属性を削除することで、それまで、move
で行っていた board_records
の要素にゲーム盤の局面のデータをコピーして追加するという処理が行われなくなったことも、処理時間の短縮の原因です。
board_records
を削除することで、change_step
の中で、特定の局面を再現するためには、ゲーム開始時の局面からその局面までの着手を行うという処理が必要になります。そのため、change_step
が行う処理は、board_records
を削除することで、処理に必要な時間が増加しています。そのことが AI の処理時間の増加につながるのではないかと心配する人がいるかもしれませんが、そのようなことはおきません。
その理由は、change_step
が GUI のリプレイ機能のボタンを押した場合でのみ呼び出されるため、AI が着手を行う際に呼び出されることはないからです。
他の修正方法
他の修正方法として、restart
メソッドと move
メソッドに、True
を代入することで records
属性を変更しない ようにする 仮引数を追加 するという方法が考えられますが、上記で紹介した方法よりも実装が面倒なので、本記事では採用しません。また、marubatsu.ipynb にも記述しないので、興味がある方は自分で記述してみて下さい。
具体的には、restart
メソッドを下記のように修正します。
-
1 行目:仮引数に、デフォルト値を
True
とするupdate_records
を追加する。デフォルト値をTrue
としたのは、これまでのプログラムとの互換性を保つためである -
8、9 行目:
update_records
がTrue
の場合のみ、records
属性を変更する
1 def restart(self, update_records=True):
2 self.initialize_board()
3 self.turn = Marubatsu.CIRCLE
4 self.move_count = 0
5 self.status = Marubatsu.PLAYING
6 self.last_move = -1, -1
7 self.last_turn = None
8 if update_records:
9 self.records = [self.last_turn]
10
11 Marubatsu.restart = change_restart
行番号のないプログラム
def restart(self, update_records=True):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
if update_records:
self.records = [self.last_turn]
Marubatsu.restart = change_restart
修正箇所
-def restart(self):
+def restart(self, update_records=True):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
- self.records = [self.last_turn]
+ if update_records:
+ self.records = [self.last_turn]
Marubatsu.restart = change_restart
プログラムは省略しますが、move
メソッドも同様の方法で修正します。
次に、change_step
を下記のプログラムの 4、6 行目のように修正し、restart
メソッドと move
メソッドを実行しても records
属性の値が変化しない ようにします。
1 def change_step(self, step):
2 # step の範囲を正しい範囲に修正する
3 step = max(0, min(len(self.records) - 1, step))
4 self.restart(update_records=True)
5 for x, y in self.records[1:step+1]:
6 self.move(x, y, update_records=True)
7
8 Marubatsu.change_step = change_step
行番号のないプログラム
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
self.restart(update_records=True)
for x, y in self.records[1:step+1]:
self.move(x, y, update_records=True)
Marubatsu.change_step = change_step
修正箇所
def change_step(self, step):
# step の範囲を正しい範囲に修正する
step = max(0, min(len(self.records) - 1, step))
- self.restart()
+ self.restart(update_records=True)
for x, y in self.records[1:step+1]:
- self.move(x, y)
+ self.move(x, y, update_records=True)
Marubatsu.change_step = change_step
他にも方法はあるかもしれませんので、本記事で紹介した方法よりも良い方法を思いついた方は実装し、コメントで知らせてくれるとうれしいです。
今回の記事のまとめ
今回の記事では、GUI の機能の分離の作業を完了しました。また、差分によって局面を再現することで、インスタンスの属性に代入されたデータ量を減らし、deepcopy
によるコピーに必要な時間を減らす方法を紹介しました。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
次回の記事
-
set_button_status
は静的メソッドとして定義されていますが、静的メソッドであるかどうかはこのエラーとは関係ありません。このエラーはset_button_status
を通常のメソッドとして定義した場合でも発生します ↩