目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
イベントハンドラ内で、着手を行うプログラム
前回の記事で、イベントハンドラ内 で、マウス が 押された場所 の ゲーム盤 の マスの座標 が 計算できる ようになったので、今回の記事では イベントハンドラの中 で、そのマス に 着手を行うプログラム を 記述 します。
ローカル関数としてのイベントハンドラの定義
指定したマス に 着手 する 処理 を行う move
メソッドは Marubatsu
クラスのメソッドなので、move
メソッドを 利用 するためには、着手 を行う ゲーム盤 を表す Marubatsu
クラスの インスタンス が 必要 になります。一般的な関数 であれば、Marubatsu
クラスの インスタンス を 代入 する 仮引数を追加 すればよいのですが、残念ながら、イベントハンドラ は、イベントループ から、あらかじめ決められた実引数 を 記述 して 呼び出される という 仕組み になっているので、自分で 新しい仮引数 を 追加 することは できません1。
Marubatsu
クラスの インスタンス を、イベントハンドラの中 で 利用できる ようにする 方法 として、イベントハンドラ を、Marubatsu
クラスの メソッドの中 で、ローカル関数 として 定義 するという方法があります。以前の記事で説明したように、ローカル関数 の ブロックの中 では、その ローカル関数 が 定義された関数(または メソッド)の ブロックの中 の 名前 を そのまま利用 することが できます。
draw_board
メソッドの ブロックの中 に、ローカル関数 として on_mouse_down
を定義 することで、draw_board
メソッドの 仮引数 self
を、on_mouse_down
の ブロックの中 で 利用できます。draw_board
メソッドの 仮引数 self
には、〇×ゲーム を表す Marubatsu
クラスの インスタンス が 代入 されているので、下記 のプログラムのように、self
という 名前 を そのまま利用 して move
メソッドを呼び出して 着手を行う ことが できます。
-
13 ~ 19 行目:
on_mouse_down
を、draw_board
の ローカル関数 として 定義 する -
16、17 行目:マウス を 押した場所 の ゲーム盤の座標 を 計算 し、
x
とy
に 代入 する -
18 行目:
self
には、draw_board
メソッドを 呼び出し たMarubatsu
クラスの インスタンス が 代入 されているので、move
メソッドを 呼び出し て (x, y) の マス に 着手を行う -
19 行目:ゲーム盤の描画 を 更新 するために、
draw_board
メソッドを 呼び出す -
22 行目:画像の上 で マウスを押した際 に
on_mouse_down
の イベントハンドラ を 呼び出すようにする処理 を、on_mouse_down
の 定義の後 に 移動 する
なお、on_mouse_down
の 定義 は、22 行目 で on_mouse_down
を使って 画像 と イベントハンドラ を 結び付ける処理より も 前に記述 しなければならない点に 注意 して下さい。
1 %matplotlib widget
2 from marubatsu import Marubatsu
3 import matplotlib.pyplot as plt
4
5 def draw_board(self, size=3):
6 fig, ax = plt.subplots(figsize=[size, size])
7 fig.canvas.toolbar_visible = False
8 fig.canvas.header_visible = False
9 fig.canvas.footer_visible = False
10 fig.canvas.resizable = False
11
12 # ローカル関数としてイベントハンドラを定義する
13 def on_mouse_down(event):
14 # Axes の上でマウスを押していた場合のみ処理を行う
15 if event.inaxes:
16 x = math.floor(event.xdata)
17 y = math.floor(event.ydata)
18 self.move(x, y)
19 self.draw_board()
20
21 # fig の画像にマウスを押した際のイベントハンドラを結び付ける
22 fig.canvas.mpl_connect("button_press_event", on_mouse_down)
以下同じなので省略
23
24 Marubatsu.draw_board = draw_board
行番号のないプログラム
%matplotlib widget
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import math
def draw_board(self, size=3):
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:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board()
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
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)
# ゲーム盤を描画する
plt.show()
Marubatsu.draw_board = draw_board
修正箇所
%matplotlib widget
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
def draw_board(self, size=3):
fig, ax = plt.subplots(figsize=[size, size])
- fig.canvas.mpl_connect("button_press_event", on_mouse_down)
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:
+ x = math.floor(event.xdata)
+ y = math.floor(event.ydata)
+ self.move(x, y)
+ self.draw_board()
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
+ fig.canvas.mpl_connect("button_press_event", on_mouse_down)
以下同じなので省略
Marubatsu.draw_board = draw_board
上記 の 修正後 に、下記 のプログラムを実行し、マスの上 で マウスを押して みて下さい。下記 は、(0, 0) の マスの上 で マウスを押した 場合の 図 です。実行結果 からわかるように、(0, 0) に 〇 が配置 された、新しい画像 が 描画される ようになります。
mb = Marubatsu()
mb.play(ai=[None, None], gui=True)
実行結果
なお、これまでに グローバル関数 として定義した on_mouse_down
は 必要が無くなった ので、marubatsu.py から削除 します。
画像が毎回描画される問題の原因
ゲーム盤の マスの上 で マウスで押す ことで 着手 を 行える ようになりましたが、着手 を 行うたび に 新しい画像 が 描画 されるという 問題 が あります。このようなことが起きる 原因 は、draw_board
メソッドの 最後 で plt.show()
を 実行している からです。
以前の記事で説明したように、%matplotlib widget
を 実行 した場合は、plt.show()
を 実行 すると、画像を更新 するの ではなく、新しい画像 が 描画 されてしまうため 一般的 には plt.show()
は 実行しません。そこで、下記 のプログラムのように、draw_board
の 最後 の plt.show()
を 削除 することにします。
def draw_board(self, size=3):
元と同じなので省略
# 最後に行っていた、この下のゲーム盤を描画する `plot.show()` を削除する
Marubatsu.draw_board = draw_board
プログラム全体
def draw_board(self, size=3):
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:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board()
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
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)
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, size=3):
元と同じなので省略
# ゲーム盤を描画する
- plt.show()
Marubatsu.draw_board = draw_board
実行結果 は 先ほどと同じ なので 省略 しますが、上記 の 修正後 に、下記 のプログラムを実行し、マスの上 で マウスを押した 際に、相変わらず 新しい画像 が 描画される ことを 確認 して下さい。このようなことが起きる原因について少し考えてみて下さい。
mb.play(ai=[None, None], gui=True)
以前の記事で説明したように、%matplotlib widget
を 実行 した場合は、Figure を 作成するだけ で、画像が描画される ようになります。上記 のようなことが起きる 原因 は、下記 のプログラムのように、draw_board
メソッドの 最初の行 で、subplots
メソッドを実行して、新しい Figure を 作成 しているからです。
def draw_board(self, size=3):
fig, ax = plt.subplots(figsize=[size, size])
以下略
画像 を 更新する ためには、draw_board
で 毎回新しい Figure を 作成 するの ではなく、Figure は 一度だけ作成 し、以後 は その Figure に対して 画像 を 描画する処理 を 行う必要 が あります。どのように修正すればよいかについて少し考えてみて下さい。
play
メソッドでの Figure の作成
まず、一度だけ 行う Figure の作成 を、どこで行うか を 考える必要 があります。play
メソッドは、〇×ゲームを遊ぶ際 に 一度だけ実行 するので、play
メソッド の 最初 で その処理を行う という 方法 が 良い でしょう。
play
メソッドの修正
そこで、下記 のプログラムのように、play
メソッドの 最初 で、gui=True
の場合に Figure を 作成する ように 修正 することにします。その際に、Figure の設定 、イベントハンドラの定義 と 結びつけ の 処理 など、Figure に 関する処理 も play
メソッドで 行う必要がある 点に 注意 して下さい。
-
10 ~ 27 行目:
gui
がTrue
の場合 にdraw_board
に 記述 されていた Figure の作成、設定、イベントハンドラの定義 と 結び付け の処理を そのまま記述 する
なお、draw_board
は、play
メソッド から self.draw_board
によって 呼び出される ので、draw_board
の self
と、play
メソッドの self
には 同じ Marubatsu
クラスの インスタンス が 代入 されています。そのため、draw_board
の on_mouse_down
の 定義 を、play
メソッドに 移動しても、on_mouse_down
の中では 同じ処理 が 行われます。
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False):
2 # seed が None でない場合は、seed を乱数の種として設定する
3 if seed is not None:
4 random.seed(seed)
5
6 # 〇×ゲームを再起動する
7 self.restart()
8
9 # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
10 if gui:
11 fig, ax = plt.subplots(figsize=[size, size])
12 fig.canvas.toolbar_visible = False
13 fig.canvas.header_visible = False
14 fig.canvas.footer_visible = False
15 fig.canvas.resizable = False
16
17 # ローカル関数としてイベントハンドラを定義する
18 def on_mouse_down(event):
19 # Axes の上でマウスを押していた場合のみ処理を行う
20 if event.inaxes:
21 x = math.floor(event.xdata)
22 y = math.floor(event.ydata)
23 self.move(x, y)
24 self.draw_board()
25
26 # fig の画像にマウスを押した際のイベントハンドラを結び付ける
27 fig.canvas.mpl_connect("button_press_event", on_mouse_down)
元と同じなので省略
28
29 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# 〇×ゲームを再起動する
self.restart()
# 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:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board()
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
if verbose:
if gui:
self.draw_board()
return
else:
print(self)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# 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()
else:
print(self)
return self.status
Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# 〇×ゲームを再起動する
self.restart()
# 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:
+ x = math.floor(event.xdata)
+ y = math.floor(event.ydata)
+ self.move(x, y)
+ self.draw_board()
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
+ fig.canvas.mpl_connect("button_press_event", on_mouse_down)
元と同じなので省略
Marubatsu.play = play
draw_board
の修正
次に、draw_board
から、play
メソッドに 移動した処理 を 下記 のプログラムのように 削除 する 必要 が あります。
def draw_board(self, size=3):
# ここにあった、Figure の作成と設定、イベントハンドラの定義と結び付けを行う処理を削除する
元と同じなので省略
Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, size=3):
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
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)
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, size=3):
- 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:
- x = math.floor(event.xdata)
- y = math.floor(event.ydata)
- self.move(x, y)
- self.draw_board()
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
- fig.canvas.mpl_connect("button_press_event", on_mouse_down)
元と同じなので省略
Marubatsu.draw_board = draw_board
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、実行結果 のように エラーが発生 します。エラーの原因 について少し考えてみて下さい。
mb.play(ai=[None, None], gui=True)
実行結果
略
Cell In[5], line 11
9 # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
10 if gui:
---> 11 fig, ax = plt.subplots(figsize=[size, size])
12 fig.canvas.toolbar_visible = False
13 fig.canvas.header_visible = False
NameError: name 'size' is not defined
仮引数 size
の移動
上記 の エラー は、play
メソッドの 下記の行 で、Figure を 作成するため に subplots
メソッドを 呼び出す際 に 実引数に記述 した size
が 定義されていない ことが 原因 です。
fig, ax = plt.subplots(figsize=[size, size])
この size
は、draw_board
メソッドの 仮引数 でしたが、Figure の 作成の処理 を play
メソッドに 移動した ので、play
メソッドの 仮引数に移動 する 必要 が あります。そこで、下記 のプログラムのように、play
メソッドに、draw_board
メソッドの 仮引数 size
を そのままの形 で デフォルト引数 として 追加 することにします。
-
1 行目:仮引数 に デフォルト値 を
3
とする デフォルト引数size
を 追加 する
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
以下同じなので省略
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()
# 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:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board()
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
if verbose:
if gui:
self.draw_board()
return
else:
print(self)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# 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()
else:
print(self)
return self.status
Marubatsu.play = play
修正箇所
-def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False):
+def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
以下同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、実行結果 のように 別のエラーが発生 し、その後 で ゲーム盤 が 描画されていない画像 が 表示される ようになります。エラーの原因 について少し考えてみて下さい。
mb.play(ai=[None, None], gui=True)
実行結果
略
Cell In[6], line 3
1 def draw_board(self, size=3):
2 # y 軸を反転させる
----> 3 ax.invert_yaxis()
5 # 枠と目盛りを表示しないようにする
6 ax.axis("off")
NameError: name 'ax' is not defined
draw_board
への仮引数 ax
の追加
上記 の エラー は draw_board
メソッドの中の ax.invert_yaxis()
を 実行 する際に、ax
が 定義されていない(is not defined)ことが 原因 です。また、draw_board
の中で ax
が 定義されなくなった のは、ax
に 値を代入 する 下記 の subplots
を呼び出す 処理 を、play
メソッドに 移動 したことが 原因 です。
fig, ax = plt.subplots(figsize=[size, size])
上記のように、定義されていない名前 を 利用 するプログラムを 記述 した場合は、VSCode では、下図 のように、オレンジ色 の 波線 が 表示 されます
また、波線の上 に マウスを移動 すると、下図 のように、その原因 が 表示 されるので、波線 に 気づいた場合 は 参考にすると良い でしょう。
なお、VSCode では、赤色の波線 は 文法エラー を表すので、修正しなければ プログラムを 実行 することが できません。一方、オレンジ色の波線 は何かが おかしい可能性がある という 警告 なので、間違っている とは 限りません。また、オレンジ色の波線 が 表示されても、その文が 実行されなければ エラーは 発生しません。
そこで、play
メソッドの ax
を draw_board
メソッドに 伝える ことが できるようにする ために、draw_board
メソッドに 仮引数 ax
を 追加 し、play
メソッド内で draw_board
メソッドを 呼び出す際 に、ax
を 実引数 に 記述 するように 修正 します。
下記 は、そのように draw_board
を 修正 したプログラムです。なお、仮引数 size
はもう 必要が無くなった ので 削除 しました。
def draw_board(self, ax):
元と同じなので省略
Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, ax):
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
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)
Marubatsu.draw_board = draw_board
修正箇所
-def draw_board(self, size=3):
+def draw_board(self, ax):
元と同じなので省略
Marubatsu.draw_board = draw_board
下記 は、そのように play
メソッドを 修正 したプログラムです。
-
9、15、22 行目:
draw_board
を 呼び出す際 に、実引数ax
を 記述 する
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # ローカル関数としてイベントハンドラを定義する
3 def on_mouse_down(event):
4 # Axes の上でマウスを押していた場合のみ処理を行う
5 if event.inaxes:
6 x = math.floor(event.xdata)
7 y = math.floor(event.ydata)
8 self.move(x, y)
9 self.draw_board(ax)
元と同じなので省略
10 # ゲームの決着がついていない間繰り返す
11 while self.status == Marubatsu.PLAYING:
12 # ゲーム盤の表示
13 if verbose:
14 if gui:
15 self.draw_board(ax)
16 return
17 else:
18 print(self)
元と同じなので省略
19 # 決着がついたので、ゲーム盤を表示する
20 if verbose:
21 if gui:
22 self.draw_board(ax)
23 else:
24 print(self)
25
26 return self.status
27
28 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()
# 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:
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:
# ゲーム盤の表示
if verbose:
if gui:
self.draw_board(ax)
return
else:
print(self)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# 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_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
- self.draw_board()
+ self.draw_board(ax)
元と同じなので省略
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
if verbose:
if gui:
- self.draw_board()
+ self.draw_board(ax)
return
else:
print(self)
元と同じなので省略
# 決着がついたので、ゲーム盤を表示する
if verbose:
if gui:
- self.draw_board()
+ self.draw_board(ax)
else:
print(self)
return self.status
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
を実行し、マスの上 で マウスを押す と、マウスを押すたび に、同じ画像 に マークが配置 されるように なります が、いくつかの点 で おかしな描画 が行われてしまいます。
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
上図 の 実行結果 は、(0, 0) の マスの上 で マウスを押した場合 の 図 です。図 を よく見る と 以下の 3 つ の点が おかしい ことが わかります。
- マウスを押した (0, 0) ではなく、(0, 2) のマスに マークが表示 される
- 手番 を表す 文字列 が 上部ではなく、下部に表示 される
-
手番 を表す 文字列 の
Turn
の後 に、〇 と × が 重ねて表示 される
マークと手番の表示位置が変になる理由の検証と解決
まず、マーク と 手番 の 表示位置 が 変になる理由 を 検証 することにします。そのためには、どのような法則 で 変な位置 に 表示されるか を 調べる必要 があるので、上記 に 続けて (1, 0) の マスの上 で マウスを押してみる ことにします。下図は (1, 0) の マスの上 で マウス を 押した後 に 表示 される 図 です。
図 から、今度 は以下のような表示が行われることが分かります。
- 最初に着手 した (0, 0) の 〇 が 正しいマス に 表示 されるようになる
- 今回着手 した (1, 0) の × が (1, 2) に 表示 される
- 手番 を表す 文字列 が 正しく上部 に 表示 されるが、〇 と × が 重ねて表示 される現象は 解消されない
勘の良い方 は 法則 が わかったかも しれませんが、続けて (2, 0) に 着手 を行ってみることにします。下図は (2, 0) の マスの上 で マウス を 押した後 に 表示 される 図 です。
下図は、それぞれの着手 で 表示 された 画像 を 並べた ものです。
下記 の 表 は、それぞれの着手 で、それぞれのマーク と 手番の文字列 が 表示 された 場所 を まとめたもの です。上図 と 下記の表 から 法則 を 考えてみて下さい。
1 手目 | 2 手目 | 3 手目 | 手番の文字列 | |
---|---|---|---|---|
(0, 0) | (0, 2) | (0, 0) | (0, 2) | 下部 |
(1, 0) | (1, 2) | (1, 0) | 上部 | |
(2, 2) | (2, 0) | 下部 |
上記でよくわからない人は、上記以外 の 様々なマス に 着手 を行ってみて下さい。
表示の法則
上記 の 図と表 から、着手 を 行うたび に、マーク や 手番 を表す 文字列 の 表示位置 が 上下 で 反転する ことが 分かります。そのこと に 気づく ことが できれば、上下 を 反転する ような 処理 が 原因 である 可能性が高い ことが 推測できる ようになります。
これまでに記述 したプログラムの中で、上下 を 反転する処理 は、draw_board
メソッドの 最初で記述 した 下記 プログラムの 3 行目 の Axes の y 軸 を 反転 する処理です。
1 def draw_board(self, ax):
2 # y 軸を反転させる
3 ax.invert_yaxis()
4
5 # 枠と目盛りを表示しないようにする
6 ax.axis("off")
以下略
draw_board
メソッドは、着手 を 行うたび に 実行される ので、ax.invert_yaxis()
によって、着手 を 行うたび に ゲーム盤の画像 の 上下 が 反転する ことになります。
Axes の y 軸 を 反転する処理 は、以前の記事で説明したように、必要な処理 ですが、Figure を 作成した際 に、一度だけ 行えば 十分 です。従って、この処理 を play
メソッド に 移動 することで、問題を解決 することが できます。なお、上記 のプログラムの 6 行目 の 枠と目盛り を 表示しない ようにする ax.axis("off")
という 処理 は、draw_board
の中 で 記述 しても、元々 表示されていない枠と目盛り を、もう一度表示しないようにする ことになるので、このままでも 特に 問題はありません が、この処理 も Figure を 作成した際 に 一度 だけ 行えば良い処理 なので、ついでに play
メソッドに 移動する ことにします。
play
メソッドと draw_board
メソッドの修正
下記は、そのように draw_board
メソッドを修正したプログラムです。
def draw_board(self, ax):
# ここにあった、y 軸の反転処理と、枠と目盛りを表示しないようにする処理を削除する
元と同じなので省略
Marubatsu.draw_board = draw_board
プログラム全体
def draw_board(self, ax):
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
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)
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, ax):
# y 軸を反転させる
- ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
- ax.axis("off")
元と同じなので省略
Marubatsu.draw_board = draw_board
下記は、そのように play
メソッドを修正したプログラムです。
- 6 行目:Axes の y 軸 を 反転する処理 を 追加 する
- 9 行目:Axes の 枠と目盛り を 表示しないようにする処理 を 追加 する
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # fig の画像にマウスを押した際のイベントハンドラを結び付ける
3 fig.canvas.mpl_connect("button_press_event", on_mouse_down)
4
5 # y 軸を反転させる
6 ax.invert_yaxis()
7
8 # 枠と目盛りを表示しないようにする
9 ax.axis("off")
元と同じなので省略
10
11 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()
# 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:
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)
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
if verbose:
if gui:
self.draw_board(ax)
return
else:
print(self)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# 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):
元と同じなので省略
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# y 軸を反転させる
+ ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
+ ax.axis("off")
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
を実行し、マスの上 で マウスを押す と、上下が反転 する 問題点 が 修正 されていることが 確認 できます。ただし、手番の文字列 の中で 〇 と × が 重なって表示 される 問題 は 解決できていません。
下記 の 実行結果 は、(0, 0)、(1, 0)、(0, 2) の 順 で マスの上 で マウスを押した場合 の図で、正しいマス に 着手 が 行われている ことが 確認 できます。実際 に JupyterLab 上で 様々なマスに着手を行って 確認 してみて下さい。
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
〇 と × が重なって表示される問題の原因と解決
%matplotlib widget
を 実行 することで、Figure の内容 を 描画 した 画像 に 対して、線 や 文字列 などを 後から描画 して 更新 することが できる ように なります。その際 に、当然ですが、それまで に 描画した内容 は 残り続けます。
そのため、draw_board
メソッドを 実行するたび に、ゲーム盤の画像 を表す Figure に「ゲーム盤の枠線」、「配置されたマーク」、「手番の情報」が 描画 されますが、その際 に それ以前 に 描画した内容 が 自動的 に 削除される ことは ありません。
従って、〇 と × が 重なって表示 される 問題 は、それ以前 の 手番 で draw_board
メソッドを 実行した際 に 描画 した 手番を表す文字列 の 上 に、新しい手番を表す文字列 が 重ねて描画された ことが 原因 です。
この問題 は、draw_board
メソッドで ゲーム盤の画像 を 描画する際 に、Figure に対して 登録 した 線や文字列 などの Artist の情報 を 削除 することで 解決 することができます。
別の言葉 で説明すると、ゲーム盤 を 描画するたび に、それまで に 描画した内容 を すべて消して から、改めて ゲーム盤を 一から描画し直す ということです。
その処理 は、Axes の clear
メソッド2を 呼び出す ことで 行うことができる ので、その処理 を、下記 のプログラムのように、draw_board
の 最初に記述 します。
-
3 行目:
ax
のclear
メソッドを 実行 して、Axes の 内容 を クリア する
def draw_board(self, ax):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
元と同じなので省略
Marubatsu.draw_board = draw_board
プログラム全体
def draw_board(self, ax):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
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)
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, ax):
# Axes の内容をクリアして、これまでの描画内容を削除する
+ ax.clear()
元と同じなので省略
上記 の 修正後 に、下記 のプログラムで play
を実行すると、実行結果 のように、Axes の 枠と目盛 りが 表示され、手番の情報 が 下部に表示される ようになってしまいます。また、目盛り を よく見る ことでも、y 軸 の 上下の反転 が 行われていない ことが わかります。
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
Axes の clear
メソッドの詳細については、下記のリンク先を参照して下さい。
clear
メソッドが行う処理
上記 のような 問題が発生 する 理由 は、Axes の clear
メソッドが、Axes への Artist の 登録 の情報 だけでなく、軸の反転 や、目盛りの非表示 など、Axes に対して行った すべての処理 を クリアしてしまう からです。
そのため、Axes の clear
メソッドを 実行した場合 は、下記 のプログラムの 6、9 行目 のように、軸の反転 と 枠と目盛りの非表示 の 処理 を 改めて実行 する 必要 が あります。
1 def draw_board(self, ax):
2 # Axes の内容をクリアして、これまでの描画内容を削除する
3 ax.clear()
4
5 # y 軸を反転させる
6 ax.invert_yaxis()
7
8 # 枠と目盛りを表示しないようにする
9 ax.axis("off")
元と同じなので省略
10
11 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, ax):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
ax.axis("off")
# 上部のメッセージを描画する
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn
# 決着がついていれば勝者を表示する
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)
Marubatsu.draw_board = draw_board
修正箇所
def draw_board(self, ax):
# Axes の内容をクリアして、これまでの描画内容を削除する
ax.clear()
# y 軸を反転させる
+ ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
+ ax.axis("off")
元と同じなので省略
Marubatsu.draw_board = draw_board
軸の反転 と 枠と目盛りの削除 の 処理 を draw_board
で行う ようにしたので、下記 のプログラムのように、play
メソッドから それらの処理 を 削除 することにします。
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# この下にあった、軸の反転と枠と目盛りの非表示の処理を削除する
元と同じなので省略
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()
# 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:
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:
# ゲーム盤の表示
if verbose:
if gui:
self.draw_board(ax)
return
else:
print(self)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# 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):
元と同じなので省略
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# y 軸を反転させる
- ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
- ax.axis("off")
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、今度は 問題なく 〇× ゲームを 遊べるようになっている ことが 確認 できます。実際 に 何度か遊んでみて、問題が発生 しないかどうかを 確認 してみて下さい。
下記の 実行結果 は、決着がつく まで 着手を行った場合 の 図 です。
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
実は、上記 のプログラムには一つ 問題があります。それが何かを少し考えてみて下さい。
今回の記事で、先程 わざわざ draw_board
から play
へ移動 した、軸の反転などの処理 を、再び draw_board
へ戻す ような 手順 で 〇×ゲームを実装 したことに 疑問を覚えた人 が いるのではないか と思います。
実際 に、プログラミング に ある程度慣れた上 で、matplotlib の 仕組み を よく理解 していれば、今回の記事 のような まどろっこしい修正 を 何度も行わず に、一度 で 正しいプログラム を 記述 する事が できるかもしれません。
しかし、一度別の場所に移動 した プログラム を、後から 様々な理由で 元の場所に戻す ようなことは、実際 の プログラミング でも よくあること です。正直に告白 すると、今回の記事 を 執筆した際 に、筆者 は 実際 に 軸の反転などの処理 を、play
へ 移した後 で、移す必要がなかったこと に 気づいて 元に 戻しました 。
そのようなことの経験 は、プログラミング の 力の上達 に 役立つと思いました ので、あえて一度でうまくいく プログラムを 紹介するのではなく、今回の記事 のような 回り道になる ような 手順 を 紹介しました。
clear
メソッドは、Figure と Axes の 両方 に対して 行うことができます が、ゲーム盤 の 枠 や 文字列 を表す Artist は、ax.plot()
や ax.text()
によって 行う ことからわかるように、Axes に 対して 登録している ので、ゲーム盤の描画 の 情報 を表す Artist を 削除する場合 は、Axes の clear
メソッドを 実行します。
fig.clear()
を 実行 することでも、Figure から、ゲーム盤 の Artist の情報 を 削除 することが できます が、その場合 は、Figure に 登録 された Axes までも が 削除されてしまう3 ため、Axes の 再登録が必要 になる点に 注意 が必要です。
Figure の clear
メソッドの詳細については、下記のリンク先を参照して下さい。
決着後の着手の禁止
上記のプログラムの 問題 は、ゲーム の 決着後 も 着手 が 行えてしまう というものです。例えば、下図右 は、下図左 で 〇 が 勝利した後 に (2, 1) のマスの上で マウスを押した 場合の図で、実際 に ゲームの決着後 に 着手 が 行えています。
gui=False
を 記述(またはキーワード引数 gui
を省略)して play
メソッドを 実行した場合 に、ゲーム の 決着後 に 着手 を 行えない理由 は以下の通りです。
-
決着が付いた時点 で、
play
メソッドの中の 繰り返し処理 が 終了 する -
イベントハンドラ を 登録しない ので、
play
メソッドの 処理が終了 した時点で、play
メソッドに 関連する処理 が 完全に終了 する
一方、gui=True
を 記述 して play
メソッドを 実行した場合 は、イベントハンドラ を 登録 するため、play
メソッドの 処理が終了 しても、ゲーム盤 の 画像の上 で マウスを押す ことで、登録 した イベントハンドラ が 何度でも 呼び出されて 実行されます。
従って、ゲームの決着後 に 着手 を 行えない ようにするためには、下記 のプログラムのように、イベントハンドラ の 処理の中 で、ゲーム が 決着していない場合 のみ 着手を行う ような 処理を記述 する 必要 が あります。
-
5 行目:着手を行う場合 を 判定 する 条件式 に、ゲームの状態 を表す
self.status
が、ゲーム の 決着がついていない ことを表すMarubatsu.PLAYING
であることを 加える
1 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # ローカル関数としてイベントハンドラを定義する
3 def on_mouse_down(event):
4 # Axes の上でマウスを押していた場合で、ゲーム中の場合のみ処理を行う
5 if event.inaxes and self.status == Marubatsu.PLAYING:
6 x = math.floor(event.xdata)
7 y = math.floor(event.ydata)
8 self.move(x, y)
9 self.draw_board(ax)
元と同じなので省略
10
11 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()
# 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)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
if verbose:
if gui:
self.draw_board(ax)
return
else:
print(self)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# 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_mouse_down(event):
# Axes の上でマウスを押していた場合で、ゲーム中の場合のみ処理を行う
- if event.inaxes:
+ 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)
元と同じなので省略
Marubatsu.play = play
上記 の 修正後 に、下記 のプログラムで play
メソッドを 実行 すると、ゲーム の 決着後 にゲーム盤の 空のマスの上 で マウスを押して も 着手 を 行えなくなっている ことが 確認 できます。実際 に 何度か遊んで みて 確認 して下さい。なお、実行結果 は 省略 します。
mb.play(ai=[None, None], gui=True)
本記事 では 採用しません が、ゲーム が 決着した際 に、ipywidgets から、イベントハンドラ の 登録を削除 することで、着手 を 行えなくする という 方法 もあります。
本記事 がその方法を 採用しない理由 は、今後の記事で、ゲーム の リセットボタン を 表示 することで、play
メソッドを 新しく実行しなくても、新しいゲーム を 開始できる ようにするからです。ゲーム が 決着した際 に、ipywidgets から、イベントハンドラ の 登録を削除 すると、リセットボタンを押して 新しいゲーム を 開始する際 に、再び イベントハンドラを ipywidgets に 登録する必要 が 生じてしまいます。
なお、ipywidgets から、イベントハンドラ の 登録を削除 する方法は、mpl_disconnect
というメソッドを 利用 します。
mpl_disconnect
の詳細は、下記のリンク先を参照して下さい。
フロー駆動型とイベント駆動型のフローチャート
2024/04/08:図 と 説明 が 一部間違っていた ので 修正 しました。
処理の流れ が良く わからなくなっている人 が いるかもしれません ので、gui
が False
の場合に play
メソッドを実行した際に行われる フロー駆動型プログラミングの処理 と、gui
が True
の場合に play
メソッドを実行した際に行われる イベント駆動型プログラミングの処理 の フローチャート を示します。
下図 は、play
メソッドの フローチャート です。オレンジ色 の 長方形 が、play
メソッドが 終了する処理 であることを表します。なお、ゲームの 途中経過 や、結果 を 表示 する場合の 処理の流れ を 示したい ので、verbose
には True
が 代入 されているものとして verbose
に 関する処理 は 省略 しました。また、テキストボックスに exit を入力 して ゲーム を 途中で終了 する 処理など の 例外的な処理 も 省略 しました。
フロー駆動型の play
メソッドのフローチャート
CUI で 〇×ゲーム を 遊ぶ場合 は、下記 の 方法 で play
メソッドを 実行 します。
-
%matplotlib widget
を 実行しない(既に実行していた場合は、%matplotlib inline
を 実行 して %matplotlib widget を 実行する前 の 状態に戻す) -
仮引数
gui
に 対応 する 実引数 を 省略 するか、gui=False
を 記述 して実行する
この場合は、play
メソッドの フローチャート は、下図 のような フロー駆動型 の処理を 行います。なお、図 の 点線 と 灰色の図形 は、その部分が 実行されない ことを 表します。
フロー駆動型 では %matplotlib widget
を 実行しない ので、イベントループ は 実行されません。そのため、play
メソッドの 処理 が 終了した時点 で すべての処理 が 終了 します。
イベント駆動型の play
メソッドのフローチャート
GUI で 〇×ゲーム を 遊ぶ場合 は、下記 の 方法 で play
メソッドを 実行 します。
-
%matplotlib widget
を 実行する -
gui=True
を 記述 して実行する
この場合は、play
メソッドの フローチャート は、下図 の 赤線の処理 を行い、イベントハンドラ の 登録 を 行った後 で、着手を行う前 に play
メソッドの 処理が終了 します。
図の「決着がついた?」が no になる のは、以下 のような 理由 です。
- ゲーム開始時 の 局面 は 決着がついていない
-
次の繰り返し処理 が 行われる前 に
play
メソッドの 処理が終了 するので、「決着がついた?」が yes に なることはない
%matplotlib widget
を 実行する と、下図 の フローチャート の処理を行う、イベントループ の 処理 が 実行 され、イベント駆動型 の 処理 が 行われる ようになります。
play
メソッドの 処理の中 で、ゲーム盤 の 画像の上 で マウスを押した際 に 実行 する イベントハンドラ を 登録 したので、その操作 を 行うたび に、イベントループ から、下図 の フローチャート の イベントハンドラ の 処理 が 呼び出され て 実行 されます。
イベントループ の フローチャート からわかるように、イベントループ の 処理 は 終了しない ので、登録 した イベントハンドラ は、対応 する イベント が 発生するたび に、何度でも 呼び出されて 実行 されます。そのため、play
メソッドが 終了した後 で、画像の上 で マウス を 押すたび に 着手を行う 処理が 実行 されます。
フロー駆動型とイベント駆動型の違いのまとめ
フロー駆動型 と イベント駆動型 の 違い は以下の通りである。
- フロー駆動型 は、実行 した プログラムが終了 すると、すべての処理 が 終了する
- イベント駆動型 は、イベントハンドラ を 登録 することによって、実行 した プログラム の 終了後 に、イベントループ によって、 登録 した イベントハンドラの処理 が 何度でも実行される
今回の記事のまとめ
今回の記事では、イベント駆動型プログラミング の 手法 で、マウスを押す ことで 着手を行う という、〇×ゲーム の GUI の 入力部分 を 実装 し、GUI で 人間どうしの対戦 が 行える ようにしました。次回の記事 では、GUI で AI と対戦 する 処理を記述 します。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
次回の記事
更新日時 | 更新内容 |
---|---|
2024/4/8 | フローチャートの図と説明が一部間違っていたので修正しました |