目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
ボタンによるゲームのリセット
前回の記事で、〇×ゲーム を GUI で遊べる ようになりました。今回の記事では、ipywidgets を利用して、ゲーム を リセットする ための ボタン を 実装 することにします。
リセットボタンの必要性
これまで の記事では、新しいゲーム を 開始 するたびに、play
メソッドを 実行 してきましたが、その際 に 下記 の メッセージが表示 されたことはないでしょうか。なお、実際のメッセージ は 1 行 で表示されますが、わかりやすいよう に 下記 では 改行 を行っています。
C:\Users\ys\AppData\Local\Temp\ipykernel_1672\1311249836.py:6: RuntimeWarning:
More than 20 figures have been opened. Figures created through the pyplot interface
(`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume
too much memory. (To control this warning, see the rcParam `figure.max_open_warning`).
Consider using `matplotlib.pyplot.close()`.
plt.subplots(figsize=[0.1, 0.1])
上記 のメッセージの中の、下記 のメッセージの 意味 は、以下の通り です。
More than 20 figures have been opened.
Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are
retained until explicitly closed and may consume too much memory.
20 より多く(more than 20)の 画像(figures)が 開かれている(has been opened)。
pyplot の(pyplot.figure
メソッドなどの)インターフェース1(interface)よって 作成 された(created through)Figure は、明示的 に(explicitly)閉じられる(closed)まで(until)保持される(retained)ため、多く(too many)の メモリ(memory)を 消費(consume)する 可能性(may)がある。
以前の記事で説明したように、%matplotlib widget
を 実行 した場合は、作成 された Figure が、pyplot が 管理 する Figure の一覧から 自動的 に__削除__ されることは ありません。play
メソッドは、その中 で subplots
メソッドによって 新しい Figure を 作成する ため、play
メソッドを 実行するたび に Figure が 1 つ作成 されますが、作成された Figure は、すべて残り続ける ために 上記 のような 警告のメッセージ が 表示 されます。
上記 の メッセージ を 自分の目 で 確認したい 人は、下記の プログラムを 実行 してみて下さい。下記 のプログラムは、plt.subplots
によって、21 個 の Figure を 作成 するプログラムで、実行 すると 上記のメッセージ と、21 個 の 画像 が 描画 されます。なお、描画 される Figure の 画像 を 小さくするため に、figsize=[0.1, 0.1]
を 実引数に記述 しました。実行結果は長いので省略します。
%matplotlib widget
import matplotlib.pyplot as plt
import japanize_matplotlib
for i in range(21):
plt.subplots(figsize=[0.1, 0.1])
プログラム の バグ を 修正した際 に、動作を確認 するために play
メソッドを 何度も実行 することは 仕方がありません が、〇×ゲーム の 新しいゲーム を 遊ぶため に play
メソッドを 毎回実行 すると、実行した回数 だけ Figure が 作成される ことになるため、その分だけコンピューターの メモリ が 消費されてしまう ことになります。そこで、ipywidgets の ボタン の クリック で 〇×ゲーム を リセットできる ようにすることで、play
メソッドを 実行しなくても、新しいゲームを始める ことが できるよう にします。
ボタンの作成とイベントハンドラの登録のおさらい
ipywidgets の ボタン は、以前の記事で説明した方法で、下記 のプログラムで作成し、JupyterLab に表示 することが できます。
import ipywidgets as widgets
button = widgets.Button(description="Click me")
display(button)
実行結果(下図は、画像なので操作することはできません)
作成 した ボタン を クリック した際に 実行 する イベントハンドラ は、以前の記事で説明した方法で、下記 のプログラムのように 定義 して、ボタン に 結びつける ことが できます。実行結果は、上記のプログラムを実行した後で、ボタンを 3 回クリックした場合の図です。
def on_button_clicked(b):
print("ボタンがおされたよ!")
button.on_click(on_button_clicked)
実行結果(下図は、画像なので操作することはできません)
〇×ゲームのリセットボタンの表示
〇×ゲーム の リセットボタン は play
メソッドを 実行 した際に 表示 するので、下記 のプログラムのように、上記の処理 を、play
メソッドで Figure を 作成する前 に 記述 します。
-
8 ~ 16 行目:
gui
がTrue
の場合に、上記の処理 を、play
メソッドの中に 記述 する
1 from marubatsu import Marubatsu
2 import math
3
4 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
5 # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
6 if gui:
7 # リセットボタンを配置する
8 button = widgets.Button(description="リセット")
9 display(button)
10
11 # リセットボタンのイベントハンドラを定義する
12 def on_button_clicked(b):
13 print("ボタンがおされたよ!")
14
15 # イベントハンドラをリセットボタンに結びつける
16 button.on_click(on_button_clicked)
17
18 fig, ax = plt.subplots(figsize=[size, size])
元と同じなので省略
19
20 Marubatsu.play = play
行番号のないプログラム
from marubatsu import Marubatsu
import math
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# 〇×ゲームを再起動する
self.restart()
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# リセットボタンを配置する
button = widgets.Button(description="リセット")
display(button)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
print("ボタンがおされたよ!")
# イベントハンドラをリセットボタンに結びつける
button.on_click(on_button_clicked)
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)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# ゲームの決着がついていない間繰り返す
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:
self.draw_board(ax)
# 手番を人間が担当する場合は、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:
self.draw_board(ax)
else:
print(self)
return self.status
Marubatsu.play = play
修正箇所
from marubatsu import Marubatsu
import math
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# リセットボタンを配置する
+ button = widgets.Button(description="リセット")
+ display(button)
# リセットボタンのイベントハンドラを定義する
+ def on_button_clicked(b):
+ print("ボタンがおされたよ!")
# イベントハンドラをリセットボタンに結びつける
+ button.on_click(on_button_clicked)
fig, ax = plt.subplots(figsize=[size, size])
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、実行結果 のように、リセットボタン が ゲーム盤の上 に 表示 されるようになり、リセットボタン を クリックするたび に、実行結果 のように メッセージ が 表示される ようになります。実際 に下記のプログラムを実行して 確認 してみて下さい。なお、下記 は 人間どうし の 対戦 です。
mb = Marubatsu()
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
実は、ボタン に 関する処理 を、Figure を 作成する処理 の 後に記述 しても、実行結果 は 変化せず、ボタン は ゲーム盤 の 画像より上 に 表示 されます。ウィジェット の 表示 の レイアウト を 変える方法 については 今後の記事 で 紹介 します。
リセットボタンのクリックによるゲームのリセットの処理
リセットボタン を クリック した時の 処理 は、Marubatsu
クラスの restart
メソッドによって 行う ことが できます。また、上記 のプログラムでは、on_button_clicked
の イベントハンドラ を、play
メソッドの ローカル関数 として 定義 したので、on_mouse_down
と 同様 に、Marubatsu
クラスの インスタンス が 代入 された play
メソッドの 仮引数 self
を そのまま利用 することが できます。従って、on_button_clicked
を 下記 のように 修正 する事で、リセットボタン を クリック すると 〇×ゲーム を リセットできる ようになります。
-
4 行目:リセットボタン が 押された場合 に呼び出される
on_button_clicked
で、restart
メソッドを呼び出して ゲーム を リセットする
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # リセットボタンのイベントハンドラを定義する
3 def on_button_clicked(b):
4 self.restart()
元と同じなので省略
5
6 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# 〇×ゲームを再起動する
self.restart()
# リセットボタンを配置する
button = widgets.Button(description="リセット")
display(button)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
# イベントハンドラをリセットボタンに結びつける
button.on_click(on_button_clicked)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
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 のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
if self.status == Marubatsu.PLAYING and ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
self.draw_board(ax)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# ゲームの決着がついていない間繰り返す
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:
self.draw_board(ax)
# 手番を人間が担当する場合は、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:
self.draw_board(ax)
else:
print(self)
return self.status
Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
- print("ボタンがおされたよ!")
+ self.restart()
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 し、いくつか のマスに 着手 を 行った後 で、リセットボタン を クリック してみて下さい。
mb.play(ai=[None, None], gui=True)
残念ながら リセットボタン を クリック しても 画面の表示 は 変化しません。しかし、その後 で マスの上 で マウスを押す と、押したマス以外 の マーク が リセット されるという、一見 すると 不思議なこと がおきます。下図右 は、下図左 の 局面 で リセットボタン を クリックした後 で、(1, 1) の マスの上 で マウスを押した 場合です。何故このようなことが起きるかについて少し考えてみて下さい。
問題の検証と修正
上記の問題 は、restart
メソッドによって 〇×ゲーム を リセットした後 で、ゲーム盤の描画 を 更新していない ことが 原因 です。
これまでのプログラム で 着手を行った後 に ゲーム盤の描画 が 更新 されていたのは、下記 のプログラムの 8 行目 のように、move
メソッドで 着手 を 行った後 で、draw_board
メソッドを 実行 して ゲーム盤の画像 を 更新する処理 を行っていたからです。
1 # ローカル関数としてイベントハンドラを定義する
2 def on_mouse_down(event):
3 # Axes の上でマウスを押していた場合で、ゲーム中の場合のみ処理を行う
4 if event.inaxes and self.status == Marubatsu.PLAYING:
5 x = math.floor(event.xdata)
6 y = math.floor(event.ydata)
7 self.move(x, y)
8 self.draw_board(ax)
以下略
初心者の方でコンピュータは賢いはずだから、ゲーム を リセット したら 気を利かせて画面の描画 を 更新してくれる だろうと 考える人 が いるかもしれません が、プログラム は、記述 した事 しか行わない 点に 注意 して下さい。ゲーム盤 の 状況が変化 した際に、このような 描画の更新 の し忘れ は よくあるミス なので紹介しました。
下記 のプログラムの 5 行目 のように、restart
メソッドを 呼び出した後 で、draw_board
メソッドを 呼び出す ように 修正 することで、この問題 を 解決 することが できます。
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # リセットボタンのイベントハンドラを定義する
3 def on_button_clicked(b):
4 self.restart()
5 self.draw_board(ax)
6
元と同じなので省略
7
8 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# 〇×ゲームを再起動する
self.restart()
# リセットボタンを配置する
button = widgets.Button(description="リセット")
display(button)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.draw_board(ax)
# イベントハンドラをリセットボタンに結びつける
button.on_click(on_button_clicked)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
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 のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
if self.status == Marubatsu.PLAYING and ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
self.draw_board(ax)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# ゲームの決着がついていない間繰り返す
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:
self.draw_board(ax)
# 手番を人間が担当する場合は、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:
self.draw_board(ax)
else:
print(self)
return self.status
Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
+ self.draw_board(ax)
元と同じなので省略
Marubatsu.play = play
下図は、修正後 の on_button_clicked
のフローチャートです。
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 し、いくつかのマス に 着手 を 行った後 で、リセットボタン を クリック することで、問題 が 解決 されたことを 確認 して下さい。なお、実行結果は省略します。
mb.play(ai=[None, None], gui=True)
上記の修正 で、リセットボタン の 処理 が 実装 できた 思う人 が いるかもしれません が、実は 上記 のプログラムには 問題があります。それが何かを少し考えてみて下さい。
リセットボタンの処理の問題点
上記 のプログラムの 問題点 は、先手 である 〇 を AI が担当 した際に、リセット後 に AI が 着手を行わない というものです。下記 の ai2
VS ai2
のプログラムを 実行 すると、実行結果 のように、AI どうし の 対戦結果 が 表示 されます。この 処理 には 問題 は ありません。
from ai import ai2
mb.play(ai=[ai2, ai2], gui=True)
実行結果(実行結果はランダムなので下記とは異なる場合があります)
下図 は、上記 の 実行結果 で、リセットボタン を クリック した場合の 図 です。図 のように、AI どうし の対戦であるにも 関わらず、着手 が 行われない ことが 確認 できます。
また、上図 の ゲーム盤の上 で マウスを押す と、そのマスに 着手 が 行われた後 で、× を担当 する AI が 着手 を 行います。下図 は、上図 で (1, 1) に 着手 を 行った場合 の図です。図のように、× を担当 する AI が (2, 2) のマスに 着手 を 行います。なお、ai2
は ランダムな着手 を行うので、下図 とは 異なるマス に 着手 が行われる 場合 が あります。
以後 も、ゲーム盤の上 で マウス を押して 〇 の着手 を 行う と、× を担当 する AI が 着手 を 行います。このように、最初 は AI どうし の 対戦 を行っていたにも 関わらず、リセットボタン を クリック すると、人間 VS AI の 対戦 が 行われる という 問題が発生 しています。
上記 の 問題点 を まとめる と 以下 のようになります。
- AI VS AI の 対戦 を行った場合、リセットボタン の クリック後 に AI が着手 を 行わない
- ゲーム盤の上 で マウスを押す と、人間 が 〇 の 着手 を 行えてしまう
- 人間 が 〇 の着手 を 行った後 で、AI が × の着手 を 行う
- その後 も、人間 が 〇 の着手 を 行えてしまう
- 結果として、人間 VS AI の 対戦 が 行われてしまう
何故このようなことが起きるかについて少し考えてみて下さい。
リセットボタンの下に表示される文字列の削除
上記の問題の原因を説明する前に、リセットボタン の 下に表示 される 文字列 について 説明 します。リセットボタン の 下に表示 される 'x'
は、× が勝利 したことを表す play
メソッドの 返り値 です。以前の記事のノートで説明したように、JupyterLab では、セルの最後 に 記述 された 文 の 計算結果 が 表示される ようになっているので、play
メソッドの 返り値 が 下図 のように リセットボタンの下 に 表示 されます。
play
メソッドの 返り値 を 表示したくない 場合は、下記 のプログラムのように、セル の 最後の文 の 後 に ;
を記述 します。;
は、文を区切る ための 記号 で、;
の後 に 何も記述しない ことで、セルの最後 に 記述 された 文 が 空の文 に なります。セルの最後 で 空の文 を 実行 した場合は 何も表示されない ので、下記 のプログラムのように ;
の前 に 記述 した play
メソッドの 計算結果 が 表示されなくなります。
mb.play(ai=[ai2, ai2], gui=True);
実行結果(実行結果はランダムなので下記とは異なる場合があります)
以後 は、余計な表示 を 行いたくない 場合に、セル の 最後の文の後 に ;
を記述 します。
実は、play
メソッドの 返り値など は、これまでのプログラム でも 表示されていました が、あまり 目立たなかった ので、本記事の 実行結果 に 表記しません でした。
リセットボタン を 表示 した場合は、リセットボタンの下 に 表示される ため、目立ってしまう ので、上記の方法を使って 表示しないようにする ことにしました。
リセットボタンのクリック後に AI が着手を行わない問題の原因
下記 は、リセットボタン を クリック した際に 実行 される イベントハンドラ です。
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.draw_board(ax)
この イベントハンドラ の 処理 は、「ゲーム の リセット」と「ゲーム盤 の 描画の処理」を行うと 終了 します。そのため、リセットボタン を クリック した 後 に、AI が 〇 を担当 していた場合でも、AI が 着手 の 処理を行う ことは ありません。
人間が 〇 の着手を行えてしまう問題の原因
リセットボタン を クリック した場合に 実行 される イベントハンドラ の 処理が終了 すると、イベントループ の 処理が再開 されます。そのため、マウスを押す と on_mouse_down
の イベントハンドラ が 実行 され、マウス を 押したマス に 着手を行う 処理が 実行 されます。これが、人間 が 〇 の着手 を 行えてしまう問題 の 原因 です。
人間が 〇 の着手を行った後で AI が × の着手を行う原因
ゲーム盤の上 で マウスを押す と、下記 の イベントハンドラ が 実行 されます。
1 def on_mouse_down(event):
2 # Axes の上でマウスを押していた場合のみ処理を行う
3 if event.inaxes and self.status == Marubatsu.PLAYING:
4 x = math.floor(event.xdata)
5 y = math.floor(event.ydata)
6 self.move(x, y)
7 self.draw_board(ax)
8
9 # 現在の手番を表す ai のインデックスを計算する
10 index = 0 if self.turn == Marubatsu.CIRCLE else 1
11 # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
12 if self.status == Marubatsu.PLAYING and ai[index] is not None:
13 x, y = ai[index](self, **params[index])
14 self.move(x, y)
15 self.draw_board(ax)
この イベントハンドラ は、マウスが押されたマス に 着手 を 行った後 で 次の手番 を AI が担当 していた場合は、12 ~ 15 行目 で、AI が 着手を行う処理 を 行います。先程 play
メソッドで 開始 したゲームは、ai2
VS ai2
だったので、6 行目 の 〇 の 着手後 に × を担当 する AI が着手 を行い、この イベントハンドラの処理 は 終了 します。これが、人間 が 〇 の着手 を 行った後 で × を担当 する AI が 着手を行う原因 です。
その後も人間が 〇 の着手を行えてしまう原因
上記の処理 を 行った後 で、AI が 着手を行う処理 は 記述されていない ので、次 の 〇 の着手 を AI が行う ことは ありません。また、on_mouse_down
の 処理 は 終了する ので、イベントループ の 処理が再開 します。そのため、先程 と 同じ理由 で、その後 も 人間 が 〇 の着手 を 行えてしまいます。
結果として 人間 VS AI の 対戦が行われてしまう問題の原因
上記から、人間 が 〇 の着手 を 行うたび に × を担当 する AI が 着手 を行い、再び 人間 が 〇 の着手 を 行うことができる ようになります。そのため、結果 として 人間 VS AI の 対戦 が行われます。以上 が 問題の原因 です。
処理のフローチャート
下図は、リセットボタン を 押した場合 に呼び出される on_button_clicked
の 処理 の フローチャート です。図から、AI が 着手を行わない ことが わかります。
下図は、その後 に ゲーム盤の上 で マウス を 押すたび に 呼び出される on_mouse_down
の 処理 の フローチャート です。実際に行われる 赤線の処理 からわかるように、人間 の 〇 の着手 が 行われた後 で、AI が × の着手 を行い、処理が終了 します。
問題の原因 が わかりました ので、問題 を 解決する方法 について少し考えてみて下さい。
問題の解決方法
初心者 の方は、下記の 間違った 問題の 解決方法 を 思いつく人 が 多いのではないか と思いますので、間違った 問題の 解決方法 を 紹介 してから、正しい解決方法 を 説明 します。
間違った問題の解決方法
上記 の現象は、前回の記事で、人間 VS AI の 処理 を 実装する際 に 起きた、人間 が 着手を行った後 に AI が 着手を行わない という現象と 似ています です。その際には、on_mouse_down
が 着手の処理 を 行った後 で、「次の手番 を AI が担当 する場合は その AI が 着手を選択 する」という 処理を記述 することで 問題を解決 しました。
下記は、on_mouse_down
の その部分の処理 を 緑色で囲った フローチャートです。
そのため、同様の考え方 で、リセットボタン を クリック した時の イベントハンドラ に、下記 のプログラムのように ゲーム の リセットの処理 を 行った後 で「AI の手番 の場合は、その AI が 着手を選択 する」処理を記述 すればよいと 考えた人 はいないでしょうか?
-
8 ~ 13 行目:リセットボタン を クリック した場合の イベントハンドラ の 最後 に、ゲーム盤の上 で マウス を 押した場合 の
on_mouse_down
の イベントハンドラ の 中 の、現在の手番 が AI の場合 に AI が 着手を行う処理 と 同じ処理 を そのまま記述 する
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # リセットボタンのイベントハンドラを定義する
3 def on_button_clicked(b):
4 self.restart()
5 self.draw_board(ax)
6
7 # 現在の手番を表す ai のインデックスを計算する
8 index = 0 if self.turn == Marubatsu.CIRCLE else 1
9 # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
10 if self.status == Marubatsu.PLAYING and ai[index] is not None:
11 x, y = ai[index](self, **params[index])
12 self.move(x, y)
13 self.draw_board(ax)
元と同じなので省略
14
15 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# 〇×ゲームを再起動する
self.restart()
# リセットボタンを配置する
button = widgets.Button(description="リセット")
display(button)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.draw_board(ax)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
if self.status == Marubatsu.PLAYING and ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
self.draw_board(ax)
# イベントハンドラをリセットボタンに結びつける
button.on_click(on_button_clicked)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
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 のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
if self.status == Marubatsu.PLAYING and ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
self.draw_board(ax)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# ゲームの決着がついていない間繰り返す
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:
self.draw_board(ax)
# 手番を人間が担当する場合は、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:
self.draw_board(ax)
else:
print(self)
return self.status
Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.draw_board(ax)
# 現在の手番を表す ai のインデックスを計算する
+ index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
+ if self.status == Marubatsu.PLAYING and ai[index] is not None:
+ x, y = ai[index](self, **params[index])
+ self.move(x, y)
+ self.draw_board(ax)
元と同じなので省略
Marubatsu.play = play
下図左 は、on_button_clicked
の 追加した部分 を 緑色で囲った フローチャートです。下図右 の on_mouse_down
のフローチャートと 見比べて みて下さい。
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、先程と同様に、AI どうし の 試合結果 が 表示 されます。
mb.play(ai=[ai2, ai2], gui=True);
実行結果(実行結果はランダムなので下記とは異なる場合があります)
上図 で リセットボタン を クリック すると、今度 は 〇 の手番 を 担当する AI が 着手を行い、下図 のような 画像 が 表示 されます。しかし、下図 からわかるように、その次 の × を担当 する AI が 着手を行わない という 問題が発生 します。なお、ai2
はランダムな着手を行うので下図とは異なる場合があります
図は示しませんが、上図 で ゲーム盤 の 画像の上 で マウスを押す と、× の着手 が 行われた後 で、AI が 〇 の着手 を 行います。従って、この場合 は、AI VS 人間 の 対戦 が 行われる ことになります。何故このようなことが起きるかについて少し考えてみて下さい。
問題の検証と修正
下図は、リセットボタン を 押した場合 に呼び出される on_button_clicked
の 処理 の フローチャート です。図から、AI が 〇 の着手のみ を 行う ことが わかります。
下図は、その後 に ゲーム盤の上 で マウス を 押すたび に 呼び出される on_mouse_down
の 処理 の フローチャート です。実際に行われる 赤線の処理 からわかるように、人間 の × の着手 が 行われた後 で、AI が 〇 の着手 を行い、処理が終了 します。なお、下図 は、先程の 人間 VS AI の 場合 に、on_mouse_down
が 呼び出され た際に 行われる処理 と 同じ です。
この問題は、リセットボタン を クリック した際の イベントハンドラ である on_button_clicked
が、AI の着手 を 行う処理 を 1 度しか行わない 点にあります。AI どうし の 対戦 の場合は、下記 の フローチャート のように、1 度ではなく、ゲーム の 決着がつくまで 繰り返し AI の着手 を 行う必要 が あります。
下図左 は、AI どうし の 対戦 を行った場合の on_button_clicked
の 処理 を 赤線 と 紫色 の 線 で示した フローチャート です。赤色 の線は、決着がついていない 場合、紫色 の線は 決着がついた 場合の 処理の流れ です。下図右 の、修正前 の on_button_clicked
の フローチャート と 見比べて みて下さい。
下記 は 上記 の フローチャート のように on_button_clicked
を 修正 したプログラムです。
- 7 行目:ゲーム の 決着がつくまで 繰り返し 処理を行う
-
11 ~ 14 行目:AI の手番 の場合は、AI が 着手を選択 する。なお、元 のプログラムの 11 行目 の 条件式 には、
self.status == Marubatsu.PLAYING
が 記述 されていたが、その条件式 が 満たされること は、7 行目 の while 文 の 条件式 で 保証されている ので、11 行目 の 条件式から削除 した - 17 行目:人間の手番 の場合は、return 文を 記述 して、イベントハンドラ の 処理を終了 することで、イベントループを再開 して マウス による 着手 が 行えるようにする
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # リセットボタンのイベントハンドラを定義する
3 def on_button_clicked(b):
4 self.restart()
5 self.draw_board(ax)
6
7 while self.status == Marubatsu.PLAYING:
8 # 現在の手番を表す ai のインデックスを計算する
9 index = 0 if self.turn == Marubatsu.CIRCLE else 1
10 # ai が着手を行うかどうかを判定する
11 if ai[index] is not None:
12 x, y = ai[index](self, **params[index])
13 self.move(x, y)
14 self.draw_board(ax)
15 else:
16 # 人間の手番の場合は、イベントハンドラを終了する
17 return
元と同じなので省略
18
19 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# 〇×ゲームを再起動する
self.restart()
# リセットボタンを配置する
button = widgets.Button(description="リセット")
display(button)
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.draw_board(ax)
while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
self.draw_board(ax)
else:
# 人間の手番の場合は、イベントハンドラを終了する
return
# イベントハンドラをリセットボタンに結びつける
button.on_click(on_button_clicked)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
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 のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
if self.status == Marubatsu.PLAYING and ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
self.draw_board(ax)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# ゲームの決着がついていない間繰り返す
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:
self.draw_board(ax)
# 手番を人間が担当する場合は、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:
self.draw_board(ax)
else:
print(self)
return self.status
Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# リセットボタンのイベントハンドラを定義する
def on_button_clicked(b):
self.restart()
self.draw_board(ax)
+ while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ai が着手を行うかどうかを判定する
- if self.status == Marubatsu.PLAYIN and ai[index] is not None:
+ if ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
self.draw_board(ax)
+ else:
# 人間の手番の場合は、イベントハンドラを終了する
+ return
元と同じなので省略
Marubatsu.play = play
実行結果は省略しますが、上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、今度は リセットボタン を クリック する たび に AI どうし の 対戦 が 行われる ことが 確認 できます。本記事では省略しますが、人間 VS 人間、人間 VS AI、AI VS 人間 の 対戦 でも 正しく動作 することを、確認 してください。
mb.play(ai=[ai2, ai2], gui=True);
今回の記事のまとめ
今回の記事では、ipywidgets の ボタン を 利用 することで、リセットボタン を 表示 して、〇×ゲーム の リセット を行うことが できるように しました。
ただし、今回の記事 で 記述 した プログラム には、同じような内容 のプログラムが 複数の個所 で 重複 しているという 問題がある ので、次回の記事では その問題 を 修正 します。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
次回の記事
-
ここでいう インターフェース は、ユーザーインターフェースの場合と同様に 操作環境 の事を表します。具体的には pyplot に関する 操作を行う ための メソッド のことを表します ↩