目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
マウスによる着手の入力の実装
前回の記事で、ゲーム盤の 画像の上 で マウスを押した際 に 処理を行う方法 を紹介したので、その方法を使って ゲーム盤上 で マウスを押す ことで 着手を行う処理 を 実装 します。
下記は、前回の記事で説明した、イベント駆動型プログラミング の 手法 を 利用 した GUI の アプリケーション の記述の 手順 です。この手順に従って play
メソッドを 修正 することで 実装 を行います。
- 画面内 の適切な位置に ウィジェットを配置 する 処理を記述 する
-
それぞれ の ウィジェット に対して、下記の処理 を 記述 する
- ウィジェット を 操作 した際に 行う処理 を 記述 する、イベントハンドラ というプログラムを 記述 する
- ウィジェット に イベントハンドラ を 結び付ける
最初 に 人間どうし が 対戦 する場合の 処理を実装 し、その後 で AI との対戦 を 実装 することにします。
matplotlib の Figure の画像の描画が行われるタイミング
前回の記事では、ipywigets の ボタンを配置 する 例を紹介 しましたが、〇×ゲーム の場合は、ゲーム盤の画像 に対して マウスで操作 を行うので、ゲーム盤の画像 そのものが GUI の 操作の対象 となる ウィジェット になります。
play
メソッドを 実行 すると、ゲームをリセットした後 で、ゲーム盤を描画 するので、play
メソッドを 修正しなくても、ゲーム盤の画像 が 描画されるはず です。
下記は、%matplotlib widget
を 実行 し、play
メソッドに ai=[None, None]
と gui=True
を 記述 して 人間どうしの対戦 を、GUI で行う プログラムです。
%matplotlib widget
from marubatsu import Marubatsu
mb = Marubatsu()
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
ゲーム盤が描画されない理由
実行結果 には、matplotlib が作成 した Figure が描画 されますが、画像の中 に ゲーム盤 が 描画されない という 問題が発生 します。その理由 は、以下 の通りです。
-
前回の記事では、
%matplotlib widget
を 実行 すると、matplotlib が インタラクティブモード になるため、Figure の 内容が更新 されると、自動的 に 画像が更新 されると 説明したが、正確 には 画像 の 描画の更新 は、イベントループ の中 で 行われる - イベントループ は、イベントループから呼び出された イベントハンドラの処理 が 行われている間 は 中断 され、その処理 が すべて終了した後 で 再開 される
下記は、前回の記事で説明した イベントループ が 行う処理 の 手順 3 に 上記の処理 を 加えた ものです。
- ipywidgets に 登録 された イベントハンドラ に 対応するイベント が 発生しているか どうかを 調べる
- イベント が 発生していれば、対応 する イベントハンドラ を 呼び出す。発生していなけれ ば 何もしない
- Figure の描画 を 更新 する
- 手順 1 へ 戻る
また、前回の記事では説明していませんでしたが、JupyterLab のセル に プログラムを記述 して 実行した場合 にも イベントが発生 し、ipywidgets には あらかじめ、そのイベント に 対応 する 下記 のような イベントハンドラ が 登録 されています。
- JupyterLab の セルに記述 された プログラムを実行 する
従って、JupyterLab のセル に プログラムを記述 して 実行した場合 は、その処理 が 終了するまで の間は、イベントループの処理 が 中断される ため、実行したプログラム が すべて終了するまで の間は、Figure の 描画の更新 は 行われません。
先程実行 した play
メソッド は、ゲーム盤の描画 を行う、draw_board
メソッドを 呼び出し ており、draw_board
メソッドの 最後 で plt.show()
が 呼び出されています。plt.show()
は、イベントハンドラ の 処理の中で実行 された場合でも 画像の描画 を行うので、上記の 実行結果 のように、真っ白 な Figure が描画 されますが、その Figure に対して 行われた plot
メソッドなどによる 描画 は、play
メソッドの 処理が終了 して JupyterLab の セルの処理 が 終了するまで は 更新されません。play
メソッドは、ゲームの決着がつく か、exit を 入力するまで の間は 終了しない ため、真っ白の画像 が 描画される ことになります。
また、play
メソッドで表示される テキストボックス に exit を入力 して play
メソッドを 終了 すると、JupyterLab の セルの処理 が 終了 し、イベントループ の処理が 再開 されるようになるため、下図 のように ゲーム盤の画像 が 描画される ようになります。
以下 は、上記 を まとめた ものです。
ipywidgets と ipympl を 利用 した イベント駆動型プログラミング では、matplotlib の Figure の 描画の更新 は、イベントループ内 の 処理 で 行われる。
イベントループの処理 は、イベントハンドラの処理 が 行われている間 は 中断 され、イベントハンドラの処理 が 終了した後 で 再開 される。
JupyterLab の セルに記述 された プログラムの実行 も、イベントループ から 呼び出される、イベントハンドラの処理 であるため、セルのプログラム の 処理が終了するまで は、Figure の 描画の更新 は 行われない。
play
メソッドの修正
上記 のことから、gui=True
を記述して GUI で 〇×ゲームを遊ぶ 場合は、play
メソッドで ゲーム盤 を 描画する処理 を 行った後 で、play
メソッドの 処理を終了 する 必要 が あります。下記 は そのよう に play
メソッドを 修正 したプログラムです。
-
10 行目:
gui
がTrue
の場合に、draw_board
メソッドで ゲーム盤 を 描画した後 に return 文 を記述して、play
メソッドの 処理を終了 する
1 import matplotlib.pyplot as plt
2
3 def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False):
元と同じなので省略
4 # ゲームの決着がついていない間繰り返す
5 while self.status == Marubatsu.PLAYING:
6 # ゲーム盤の表示
7 if verbose:
8 if gui:
9 self.draw_board()
10 return
11 else:
12 print(self)
元と同じなので省略
13
14 Marubatsu.play = play
行番号のないプログラム
import matplotlib.pyplot as plt
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False):
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
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
修正箇所
import matplotlib.pyplot as plt
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False):
元と同じなので省略
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
if verbose:
if gui:
self.draw_board()
+ return
else:
print(self)
元と同じなので省略
Marubatsu.play = play
修正後 に 下記 のプログラムを実行すると、今度は play
メソッドの 実行が終了 し、ゲーム盤の画像 が すぐに描画 されるようになることが 確認 できます。
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
上記 のように、gui=True
の場合は、ゲーム の 決着がつく前 に return 文 で play
メソッドが 終了 して play
メソッドの 返り値 が None
になるため、gui=False
(または、キーワード引数 gui
を省略)の場合のように、play
メソッドの 返り値 が、ゲームの結果 では なくなります。従って、AI どうし の対戦の 通線成績 を 計算する場合 などで、play
メソッドの 対戦結果 を 返り値 として 知りたい場合 は、gui=True
を 記述してはいけない 点に 注意 して下さい。
イベントハンドラの定義とゲーム盤の画像への結び付け
次に、前回の記事で紹介した 方法 を使って、ゲーム盤の画像 を マウスで押した際 の 処理 を行う イベントハンドラを定義 します。このイベントハンドラ は、最終的 には マウスを押した ゲーム盤の マスに着手 を行う 処理 を行いますが、いきなり その処理を 記述 するのは 難しい ので、とりあえず、下記 のプログラムのように、前回の記事で記述した、マウスを押した Axes の座標 を 表示 するプログラムとして 定義 する事にします。
def on_mouse_down(event):
print(event.xdata, event.ydata)
次に、ゲーム盤の画像 と、この イベントハンドラ を 結び付ける必要 が あります。ゲーム盤の画像 は draw_board
メソッドで 作成 しているので、その処理の直後 に 下記 のプログラムのように、前回の記事で紹介した 方法 で、ゲーム盤の画像 の上で マウスを押した時 の イベントハンドラ を 結び付ける処理 を 記述 します。
なお、ゲーム盤の画像 の「タイトル」、「ツールバー」、「変形の操作のマーク」の 表示 は 〇×ゲーム の ゲーム盤の画像 には 必要がない ので、前回の記事で説明した 方法 で、表示しない ようにしました。ただし、マウスの座標の表示 は 残しておいたほうが この後のプログラムを記述する際に 便利 なので、しばらくの間 は 表示を残しておく ことにします。
-
3 行目:
mpl_connect
を呼び出して、ゲーム盤 の 画像の上 で マウスを押した際 に、on_mouse_down
の イベントハンドラ を 呼び出す ようにする - 4 ~ 6 行目:ゲーム盤の画像 の「タイトル」、「ツールバー」、「変形の操作のマーク」の 表示 を 行わないよう にする
1 def draw_board(self, size=3):
2 fig, ax = plt.subplots(figsize=[size, size])
3 fig.canvas.mpl_connect("button_press_event", on_mouse_down)
4 fig.canvas.toolbar_visible = False
5 fig.canvas.header_visible = False
6 fig.canvas.resizable = False
# 以下同じなので略
7
8 Marubatsu.draw_board = draw_board
行番号のないプログラム
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.resizable = False
# 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
修正箇所
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.resizable = False
# 以下同じなので略
Marubatsu.draw_board = draw_board
上記の 修正後 に 下記 のプログラムを実行し、描画 された 画像の上 で マウスを押す と、実行結果 のように、画像の下 に マウスを押した Axes の 座標が表示 されるようになります。
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
マウスを押したマスに着手を行う処理の記述
上記 では、on_mouse_down
の中で、マウスを押した Axes の 座標を表示 しましたが、マウスを押した ゲーム盤のマス に 着手を行う ためには、この座標 を、ゲーム盤 の マスの座標 に 変換 する 必要 が あります。その 変換の方法 について少し考えてみて下さい。
変換の方法 が良く わからない 人は、座標が表示 された ゲーム盤の図 を 書いてみる と良いでしょう。下図 は、以前の記事 で 描画 した、ゲーム盤 の 枠 に加えて Axes の枠 と 目盛り を 描画 した際の 図 です。
画像を書く のが 面倒な場合 は、JupterLab の 画像の上 で マウスを移動 した際に、画像の下 に 表示 される Axes の座標 を 観察する という 方法 もあります。
上図 から、例えば、ゲーム盤 の (0, 0) のマスの 座標の範囲 は、x 座標 と y 座標 が 共に 0 ~ 1 の範囲 であることが わかります。同様に、(x, y) のマスの 座標の範囲 は、x 座標 は x ~ x + 1、y 座標 は y ~ y + 1 になることが わかります。
上記 から、マウス の Axes 上 の 座標 の 小数点以下の数字 を 切り捨て て 整数 に 変換する ことで、ゲーム盤 の マスの座標 に 変換できる ことが わかりました。
数値を整数に変換する方法とその注意点
Python には、数値 を 整数 に 変換する方法 が いくつかあります が、それぞれ 異なる処理 を 行う ため、正しく 使い分ける必要 が あります。間違った方法 を 利用 してしまうと、思わぬ バグの原因 になる 場合がある ので 注意が必要 です。
なお、小数点以下 の値が存在する 数値 を 整数に変換する など、数値の中 で、特定の桁数以下 の 部分 を 無くす処理 の事を、丸める(round)と 呼びます。
以下 の説明では、整数 に 変換 する 場合のみ を 説明 しますが、例えば「100 の位 で 四捨五入」する、「小数点以下 2 桁 で 切り捨てる」など、特定の桁数 の値に 変換する場合 も あります。いずれも 桁数が異なるだけ で、考え方 は 変わりません。
切り捨て、切り下げ、床関数
「切り捨て」という 用語 は、その 名前から、小数点以下 の 数値 を 無くす という意味だと 思っている人 が いるかもしれません が、数学 での 切り捨て はそうではなく、具体的には 下記 のような 計算を行います。
- その値以下 の、最も大きな整数 を 計算 する
当然ですが、元の数値 が 整数の場合 は、その値 そのものが 計算結果 になります。
整数でない 場合は、数値 が 0 以上であるか によって、下記 のような 計算 が 行われます。
- 0 以上 の数値の場合は、小数点以下 の 数値を無くす 計算を行う
- 負の数値 の場合は、小数点以下 の 数値を無くした後 で 1 を引いた値 を計算する
言葉の説明だけでは分かりづらいと思いますので、切り捨て が行う 計算 を 図で示します。下図は、数値 を 縦方向の直線 で 図示 した場合に、0 ~ 1 未満 と -1 ~ 0 未満 の 範囲の数値 に対する 切り捨て の 計算 を表す図です。
図を見れば、切り捨て が行う 計算 が、複雑 なもの ではない ことがわかるのではないかと思います。また、負の数値 の場合は、小数点以下 の 数値を無くした後 で 1 を引いた値 になることも 理解できる のではないでしょうか。
下記は、いくつかの数値 に対して 切り捨て を行った場合の表です。
数値 | 切り捨て後の数値 |
---|---|
1 | 1 |
0.5 | 0 |
0 | 0 |
-0.5 | -1 |
-1 | -1 |
切り捨て はという 表現 は 紛らわしい ので、他にも「切り下げ」、「負の無限大への丸め」、「床関数」と呼ばれる場合があります。
負の無限大への丸め は、小数点以下 の値が 存在する場合 は、上図 のように 負の無限大 に 近いほうの整数 に 丸める計算 が 行われる からです。
床関数 の 由来 は、先程の図 で、数値 を 縦方向の直線 で 表現 した際に、0 ~ 1 未満 の数値を 0 にする計算 が、数値 を 床におろして 0 にする 処理を行う ように 見える からです。
Python では 切り捨て は、数学 に関する 様々な処理 を行うことができる math という モジュール の、floor
という 関数 で行えます。floor
は 床 を表す 英語 です。下記 のプログラムは、floor
の 使用例 です。上記の表 と 同じ値 が 計算 されることが 確認 できます。
import math
print(math.floor(1))
print(math.floor(0.5))
print(math.floor(0))
print(math.floor(-0.5)) # 0 ではなく、-1 が返される
print(math.floor(-1))
実行結果
1
0
0
-1
-1
floor
に関する詳細は、下記のリンク先を参照して下さい。
切り上げ、天井関数
数学 の「切り上げ」は、下記 のような 計算 を行います。
- その値以上 の 最も小さな整数 を 計算 する
当然ですが、元の数値 が 整数の場合 は、その値 そのものが 計算結果 になります。
整数でない 場合は、数値 が 0 以上であるか によって、下記 のような 計算 が 行われます。
- 0 以上 の 数値 の場合は、小数点以下 の 数値を無くした後 で 1 を足した値 を計算する
- 負の数値 の場合は、小数点以下 の 数値を無くす 計算を行う
先程と同様に、切り上げ が 行う計算 を 図 で示します。
下記は、いくつかの数値 に対して 切り上げ を行った場合の 表 です。
数値 | 切り上げ後の数値 |
---|---|
1 | 1 |
0.5 | 1 |
0 | 0 |
-0.5 | 0 |
-1 | -1 |
切り上げ は、他にも「正の無限大への丸め」、「天井関数」と 呼ばれる 場合があります。
正の無限大への丸め は、小数点以下 の値が 存在する場合 は、上図のように、正の無限大 に 近いほうの整数 に 丸める計算 が 行われる からです。
天井関数 の 由来 は、先程の図 で、数値 を 縦方向の直線 で 表現 した際に、0 より大きく 1 以下の数値 を 1 にする計算 が、数値 を 天井に上げて 1 にするという 処理を行う ように 見える からです。
切り上げ は、math モジュールの、ceil
という 関数 で 行えます。ceil
は、天井 を表す ceiling という 英単語が由来 です。下記 のプログラムは、ceil
の 使用例 です。上記の表 と 同じ値 が 計算される ことが 確認 できます。
print(math.ceil(1))
print(math.ceil(0.5))
print(math.ceil(0))
print(math.ceil(-0.5)) # -1 ではなく、0 が返される
print(math.ceil(-1))
実行結果
1
1
0
0
-1
ceil
に関する詳細は、下記のリンク先を参照して下さい。
偶数丸めによる四捨五入
〇×ゲームの座標の変換の際には利用しませんが、四捨五入 は 良く使われる丸めの計算 で、Python の 四捨五入 を行う 関数 は 少し特殊な計算 を行うので 説明 します。
四捨五入 は、round
という 組み込み関数 で 計算 できます(math モジュールの 関数 では ありません)。round
は、丸め や 四捨五入 を表す round という 英単語が由来 です。なお、round という 用語 に関しては この後で補足 します。
四捨五入 は、一般的 には、下記の計算 を行います。しかし、Python の round
は、下記 のような 計算 は 行わない 点に 注意 して下さい。
- 小数点以下 の 数値 が 0.5 以下 の場合は 切り捨てる
- 小数点以下 の 数値 が 0.5 より大きい 場合は、切り上げる
Python の round
は、下記 の 計算 を行います。
- 最も近い整数 を 計算 する
- 小数点以下 の 数値 が 0.5 の場合 は、最も近い整数 のうち、偶数を計算 する
このような四捨五入 の 計算方法 を 偶数丸め1と 呼びます。
このように、Python の round
を 利用する際 は、一般的 な 四捨五入 とは 少しだけ異なる計算 が 行われる点 に 注意 して下さい。
下記 は、いくつかの数値 に対して 偶数丸め による 四捨五入 を 行った場合の表 です。表のように、小数点以下 の 数値 が 0.5 の場合 は、偶数の整数 が 計算 されます。
数値 | 切り上げ後の数値 |
---|---|
2 | 2 |
1.5 | 2 |
1 | 1 |
0.5 | 0 |
0 | 0 |
-0.5 | 0 |
-1 | -1 |
-1.5 | -2 |
-2 | -2 |
下記 のプログラムは、round
の 使用例 です。上記の表 と 同じ値 が 計算 されます。
print(round(2))
print(round(1.5))
print(round(1))
print(round(0.5))
print(round(0))
print(round(-0.5))
print(round(-1))
print(round(-1.5))
print(round(-2))
実行結果
2
2
1
0
0
0
-1
-2
-2
round
に関する詳細は、下記のリンク先を参照して下さい。
0 への丸め
小数点以下 の 数値を無くす 計算のことを 0 への丸め と呼びます。由来 は、小数点以下 の 数値が存在 した場合は、下図 のように、0 に近いほうの整数 に 丸めが行われる からです。数値 が 正の場合 と 負の場合 で、下図の 矢印の方向 が 異なる点 に 注目 して下さい。
Python では、math モジュールの trunc
2 という 関数 で 0 への丸め を 計算 できます。下記は、trunc
の使用例です。floor
と trunc
を 混同しない ように 注意 して下さい。
print(math.trunc(1))
print(math.trunc(0.5))
print(math.trunc(0))
print(math.trunc(-0.5))
print(math.trunc(-1))
実行結果
1
0
0
0
-1
trunc
に関する詳細は、下記のリンク先を参照して下さい。
0 への丸め は、整数型 の クラス を表す int
を使って 行う こともできます。下記 は、int
の 使用例 です。math.trunc
と 同じ計算 が 行われます。
print(int(1))
print(int(0.5))
print(int(0))
print(int(-0.5))
print(int(-1))
実行結果
1
0
0
0
-1
int
と math.trunc
の 違い は、int
が 実引数 に 文字列 を 記述できる 点です。実際に、下記 のプログラムのように、int
は 文字列の "1"
を 整数の 1
に 変換 できます。
print(int("1"))
実行結果
1
一方、math.trunc
の 実引数 に 文字列の "1"
を 記述 すると、下記 のプログラムのように エラーが発生 します。
print(math.trunc("1"))
実行結果
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[13], line 1
----> 1 print(math.trunc("1"))
TypeError: type str doesn't define __trunc__ method
int
に関する詳細は、下記のリンク先を参照して下さい。
丸めに関する英語の表記の補足
英語 の round
は、四捨五入 という 意味 で使われることが 実際にあります が、切り捨てなど を 含めた「丸め」という 意味 で 使われること も あります。round という 用語 が表す 丸めの種類 を 明確 にしたい場合は、下記 の表のように、round に 丸めの種類 を表す 単語 を 組み合わせる ことで 区別 します3。なお、紛らわしい ですが、場合によって は round off を 四捨五入 の 意味で使う こともあるようです。
日本語 | 英語 |
---|---|
切り捨て、負の無限大への丸め | round up、round towards negative infinity |
切り上げ、正の無限大への丸め | round down、round towards positive infinity |
一般的な四捨五入 | round off、round half up |
偶数丸めによる四捨五入 | round half to even |
0 への丸め | round towards 0 |
さまざま な 丸め処理 を 扱った記事 がありましたのでリンクを 紹介 します。
〇×ゲームの座標の変換で利用する丸めの種類
数値 を 整数に変換 する 丸めの方法 を 紹介 しましたが、どの方法 で、Axes 上 の マウスの座標 を ゲーム盤 の マスの座標 に 変換すればよいか について少し考えてみて下さい。
切り上げ と 四捨五入 は 明らか に ふさわしくない ので、候補 としては 切り捨て と 0 への丸め の 2 つ が挙げられます。どちらが良いかわからない 場合は、実際 に それぞれの方法 で 処理を行う プログラムを 記述 して 確認する と良いでしょう。
0 への丸めによる座標の変換
下記 は、0 への丸め を行う int
を使って、Axes 上 の マウスを押した座標 の 小数点以下の数値 を 削除 した 値を表示 するように、on_mouse_down
を 修正 したプログラムです。
def on_mouse_down(event):
print(int(event.xdata), int(event.ydata))
修正箇所
def on_mouse_down(event):
- print(event.xdata, event.ydata)
+ print(int(event.xdata), int(event.ydata))
上記 の 修正後 に、下記 のプログラムを実行し、ゲーム盤の画像 の上の さまざまな場所 で マウス を 押してみて 下さい。
mb.play(ai=[None, None], gui=True)
上図 は、ゲーム盤 の (1, 1)、(2, 1)、(0, 1) の マスの上 で マウスを押した 場合の図です。図の下 にそれらの マスの座標 が 正しく表示 されているので、一見する と 問題がない ように 思えるかもしれません。しかし、ゲーム盤の外 で マウスを押す と、下記 のような エラーが発生 してしまいます。このエラーの原因について少し考えてみて下さい。
なお、どの部分 で マウスを押す と エラーが発生するか が良く わからない 人がいるのではないかと思いますので、この後 で エラーが発生する場所 が 分かるよう にします。
略
Cell In[14], line 2
1 def on_mouse_down(event):
----> 2 print(int(event.xdata), int(event.ydata))
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
これまで のプログラムは、エラーが発生 すると、その時点 で プログラムの処理 が 完全に終了 しましたが、イベント駆動型プログラミング では、イベントハンドラ で エラーが発生 しすると、イベントループ の 中断が再開 されるため、イベントループ の 処理 は 引き続き実行 されます。そのため、上記 の エラーが発生した後 に ゲーム盤の上 で マウスを押す と、新しく イベントハンドラ が 実行 されて、エラーメッセージの後 に マウスの座標が表示 されます。
エラーの原因
エラーメッセージ から、int
の 実引数 に 記述 した event.xdeata
の値が、NoneType
、すなわち None
になっている ことが 原因 であることが 推測 されます。
上記 のプログラムで 表示 される ゲーム盤の画像 ではこの エラーの原因 が わかりにくい ので、draw_board
メソッドの 最初の行 を、下記 のプログラムのように 変更 して Figure の 背景色 を 水色 にし、Axes の 枠と目盛り の 描画を削除 する 処理 を コメント にして 実行しない ようにすることで、原因 が わかりやすく なります。
-
2 行目:
subplots
の 実引数 に、facecolor="lightblue"
を 記述 することで、作成 する Figure の 背景色 を 水色 にする -
12 行目:
ax.axis("off")
を コメントにする ことによって、Axes の 枠と目盛り が 表示される ようにする
1 def draw_board(self, size=3):
2 fig, ax = plt.subplots(figsize=[size, size], facecolor="lightblue")
3 fig.canvas.mpl_connect("button_press_event", on_mouse_down)
4 fig.canvas.toolbar_visible = False
5 fig.canvas.header_visible = False
6 fig.canvas.resizable = False
7
8 # y 軸を反転させる
9 ax.invert_yaxis()
10
11 # 枠と目盛りを表示しないようにする
12 #ax.axis("off")
以下同じなので略
13
14 Marubatsu.draw_board = draw_board
行番号のないプログラム
def draw_board(self, size=3):
fig, ax = plt.subplots(figsize=[size, size], facecolor="lightblue")
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.resizable = False
# 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
修正箇所
def draw_board(self, size=3):
- fig, ax = plt.subplots(figsize=[size, size])
+ fig, ax = plt.subplots(figsize=[size, size], facecolor="lightblue")
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.resizable = False
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
- ax.axis("off")
+ #ax.axis("off")
以下同じなので略
Marubatsu.draw_board = draw_board
上記 の 修正後 に、下記 のプログラムを実行すると、実行結果 のような ゲーム盤の画像 が 表示 されるようになります。
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
画像 の 白い部分 が、Figure に 登録 された Axes の 表示の範囲 です。この範囲の外 の、水色の部分 は Axes の 表示 の 範囲の外 なので、水色の部分 の上で マウスを押す と、前回の記事で説明したように、イベントハンドラ の 仮引数 の xdata
と ydata
属性 の 値 は None
になります。そのことは、JupyterLab に 描画 された上図の 水色の部分 に マウスを移動 した際に、図の下 に マウスの座標 が 描画されない ことからも 確認 できます。
上図 の 白い部分 で マウスを押す と 図の下 に マスの座標 が 表示 され、水色の部分 で マウスを押す と エラーが発生する ことを 確認 してみて下さい。
エラーの修正
前回の記事で説明したように、マウス を 押した場所 が、Axes の 上であるか どうかは、イベントハンドラ の 仮引数 の inaxes
4 という 属性 で 調べる ことが できます。従って、on_mouse_down
を、下記 のプログラムの 3 行目 のように 修正 することで、Axes の中 で マウス を 押した場合だけ、マスの座標 を 表示する ようになります。
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes:
print(int(event.xdata), int(event.ydata))
修正箇所
def on_mouse_down(event):
- print(int(event.xdata), int(event.ydata))
# Axes の上でマウスを押していた場合のみ処理を行う
+ if event.inaxes:
+ print(int(event.xdata), int(event.ydata))
上記の修正後 に、下記 のプログラムを実行し、ゲーム盤 の 画像の上 の さまざまな場所 で マウスを押して みて下さい。表示される画像は先ほどと同じなので実行結果は省略しますが、今度は 白色 の Axes の中 で マウス を 押した時だけ 下部に マスの座標 が 表示 され、水色 の Axes の外 で マウスを押して も エラー が 発生しなくなります。
mb.play(ai=[None, None], gui=True)
0 への丸めの問題点
初心者の方には気が付きづらいと思いますが、0 への丸め による 座標の変換 には 1 つ問題 が あります。それは、Axes の中 の、ゲーム盤 の 外の部分 で マウスを押す と、間違ったマスの座標 に 変換される というものです。
以前の記事で説明したように、matplotlib で Axes に plot
メソッドなどで 描画 を 行った場合 は、Axes の 描画範囲 は、Axes に 登録 した すべての図形 が ぴったり収まる ような 範囲より も 少しだけ広い範囲 になるように 自動的 に 設定 されます。〇×ゲームの ゲーム盤の画像 の場合は、Axes の x 座標 と y 座標 の 範囲 は、以前の記事で 確認 したように、 -0.15 ~ 3.15 が 設定 されます。
下図5の 黄色 と 緑 の 部分 が、Axes の中 で、ゲーム盤 の 外の部分 を表します。
0 への丸め で 座標の変換 を行った場合は、この 図 の 黄色い部分 で マウスを押した 際に、間違った座標 が 表示 されます。黄色い部分 は、Axes の x 座標 または y 座標 が -0.15 ~ 0 の範囲 の 負の座標 になりますが、その範囲の数値 は 0 への丸め で 整数に変換 を行うと、0 になります。そのため、黄色い部分 で マウスを押す と、ゲーム盤の外 であるにも 関わらず、(0, 0) のような ゲーム盤の中 の 座標 が 表示 されてしまいます。
実際 に、JupyterLab に 表示 される ゲーム盤の画像 の中で、上図 の 黄色い部分 に 相当 する場所で マウスを押して、そのことを 確認 してみて下さい。
上記 の事から、0 への丸め で 座標の変換 を行ってしまうと、ゲーム盤の外 で マウスを押した際 に 着手が行われてしまう という、間違った処理 が 行われてしまう ことになります。
上記 の問題が 発生する理由 は、0 への丸め では、0 ~ 1 と -1 ~ 0 の 範囲の数値 が、どちらも 0 に なってしまう という 性質がある からです。切り捨て と 切り上げ では、異なる整数の間 の範囲の 数値 が 同じ値になる ことは ありません。
このような問題 は、些細なこと なので どうでも良い のではないかと 思った人 が いるかもしれません が、些細に思える問題 を 放置 することで、後で 思わぬバグの原因になる ことが 実際にある ので、この問題 は 修正 しておいたほうが 良い でしょう。
切り捨て(床関数)による座標の変換
上記の問題 は、下記 のプログラムのように、math.floor
による 切り捨て によって 座標を変換 することで 解決 できます。
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes:
print(math.floor(event.xdata), math.floor(event.ydata))
修正箇所
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes:
- print(int(event.xdata), int(event.ydata))
+ print(math.floor(event.xdata), math.floor(event.ydata))
上記 の 修正後 に、下記 のプログラムを実行し、先程の図 の 黄色い部分 に 相当する場所 で マウスを押す と、下記 の 実行結果 のように、x 座標 や y 座標 が 負の値 になり、ゲーム盤の中 の マス とは 異なる座標 が 表示される ようになります。
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
本記事では採用しませんが、この問題 を 解決 する 別の方法 として、Axes の x 座標 と y 座標 の 表示範囲 を 0 ~ 3 に 設定 して ゲーム盤 の 範囲外 を 表示しない ようにするという 方法 があります。
ただし、その方法を採用した場合でも、後から ゲーム盤の 範囲外 を 表示 する 可能性がある のであれば、切り捨て による 座標の変換 を 行ったほうが良い でしょう。
draw_board
の修正
問題が解決 されたので、draw_board
を 下記 のプログラムのように、Figure の 背景色 を 白に戻し、枠と目盛り を 表示しない ようにします。また、画像の下部 に表示される マウス の Axes の座標 はもう 必要がなくなった ので、表示しない ように 修正 します。
-
2 行目:
facecolor="lightblue"
を 削除 して Figure の 背景色 を 白に戻す - 6 行目:画像の下部 の マウス の Axes の座標の表示 を 行わないよう にする
- 13 行目:コメントを削除 して、枠と目盛り を 表示しない ようにする
1 def draw_board(self, size=3):
2 fig, ax = plt.subplots(figsize=[size, size])
3 fig.canvas.mpl_connect("button_press_event", on_mouse_down)
4 fig.canvas.toolbar_visible = False
5 fig.canvas.header_visible = False
6 fig.canvas.footer_visible = False
7 fig.canvas.resizable = False
8
9 # y 軸を反転させる
10 ax.invert_yaxis()
11
12 # 枠と目盛りを表示しないようにする
13 ax.axis("off")
以下同じなので略
14
15 Marubatsu.draw_board = draw_board
行番号のないプログラム
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
# 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
修正箇所
def draw_board(self, size=3):
- fig, ax = plt.subplots(figsize=[size, size], facecolor="lightblue")
+ 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
# y 軸を反転させる
ax.invert_yaxis()
# 枠と目盛りを表示しないようにする
- #ax.axis("off")
+ ax.axis("off")
以下同じなので略
Marubatsu.draw_board = draw_board
上記 の 修正後 に、下記 のプログラムを実行し、実行結果 から 意図通り の ゲーム盤 の 画像が表示される ことが 確認 できました。
mb.play(ai=[None, None], gui=True)
実行結果(下図は、画像なので操作することはできません)
今回の記事のまとめ
今回の記事では、以下 の内容を 説明 しました。
-
%matplotlib widget
を 実行 した場合に、matplotlib の 画像 の 描画が更新 される タイミング - 数値 を 整数に変換 する 丸めの種類 と 注意点
- ゲーム盤の画像 の上で マウスを押した場所 の、ゲーム盤 の マスの座標 への 変換方法
次回の記事では、マウス を 押した場所 に 着手を行う処理 を 記述 します。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。なお、on_mouse_down
は 先頭に記述 しました。
次回の記事
更新履歴
更新日時 | 更新内容 |
---|---|
2024/4/5 |
gui=True の場合に、play メソッドの返り値が None になる点に関するノートを追記しました |