0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonで〇×ゲームのAIを一から作成する その69 画像の描画のタイミングと丸めによる座標の変換

0
Last updated at Posted at 2024-04-04

目次と前回の記事

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

ルールベースの AI の一覧

ルールベースの AI の一覧については、下記の記事を参照して下さい。

マウスによる着手の入力の実装

前回の記事で、ゲーム盤の 画像の上 で マウスを押した際 に 処理を行う方法 を紹介したので、その方法を使って ゲーム盤上 で マウスを押す ことで 着手を行う処理 を 実装 します。

下記は、前回の記事で説明した、イベント駆動型プログラミング の 手法 を 利用 した GUI の アプリケーション の記述の 手順 です。この手順に従って play メソッドを 修正 することで 実装 を行います。

  1. 画面内 の適切な位置に ウィジェットを配置 する 処理を記述 する
  2. それぞれ の ウィジェット に対して、下記の処理 を 記述 する
    1. ウィジェット を 操作 した際に 行う処理 を 記述 する、イベントハンドラ というプログラムを 記述 する
    2. ウィジェット に イベントハンドラ を 結び付ける

最初 に 人間どうし が 対戦 する場合の 処理を実装 し、その後 で 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 に 上記の処理 を 加えた ものです。

  1. ipywidgets に 登録 された イベントハンドラ に 対応するイベント が 発生しているか どうかを 調べる
  2. イベント が 発生していれば、対応 する イベントハンドラ を 呼び出す。発生していなけれ ば 何もしない
  3. Figure の描画 を 更新 する
  4. 手順 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 モジュールの trunc2 という 関数 で 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 の 上であるか どうかは、イベントハンドラ の 仮引数 の inaxes4 という 属性 で 調べる ことが できます。従って、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 になる点に関するノートを追記しました
  1. 銀行員 が 良く使っていた ので、「銀行丸め」 と 呼ぶ場合 も あります ↩

  2. trunc は、切り詰める という 意味 の truncate という 英単語の略 です ↩

  3. この 表以外 の 英語表記 も 存在します ↩

  4. inaxes の名前は、Axes の 中(in)に あるか どうかという 意味 です ↩

  5. この 図 は 説明のため に 独自に作った図 なので、JupyterLab には 表示されません ↩

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?