LoginSignup
0
0

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

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セルの処理終了 し、イベントループ の処理が 再開 されるようになるため、下図 のように ゲーム盤の画像描画される ようになります。

以下 は、上記まとめた ものです。

ipywidgetsipympl利用 した イベント駆動型プログラミング では、matplotlibFigure描画の更新 は、イベントループ内処理行われる

イベントループの処理 は、イベントハンドラの処理行われている間中断 され、イベントハンドラの処理終了した後再開 される。

JupyterLabセルに記述 された プログラムの実行 も、イベントループ から 呼び出されるイベントハンドラの処理 であるため、セルのプログラム処理が終了するまで は、Figure描画の更新行われない

play メソッドの修正

上記 のことから、gui=True を記述して GUI〇×ゲームを遊ぶ 場合は、play メソッドで ゲーム盤描画する処理行った後 で、play メソッドの 処理を終了 する 必要あります下記そのようplay メソッドを 修正 したプログラムです。

  • 10 行目guiTrue の場合に、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 + 1y 座標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 という 用語 に関しては この後で補足 します。

四捨五入 は、一般的 には、下記の計算 を行います。しかし、Pythonround は、下記 のような 計算行わない 点に 注意 して下さい。

  • 小数点以下数値0.5 以下 の場合は 切り捨てる
  • 小数点以下数値0.5 より大きい 場合は、切り上げる

Pythonround は、下記計算 を行います。

  • 最も近い整数計算 する
  • 小数点以下数値0.5 の場合 は、最も近い整数 のうち、偶数を計算 する

このような四捨五入計算方法偶数丸め1呼びます

このように、Pythonround利用する際 は、一般的四捨五入 とは 少しだけ異なる計算行われる点注意 して下さい。

下記 は、いくつかの数値 に対して 偶数丸め による 四捨五入行った場合の表 です。表のように、小数点以下数値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 の使用例です。floortrunc混同しない ように 注意 して下さい。

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

intmath.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表示範囲の外 なので、水色の部分 の上で マウスを押す と、前回の記事で説明したように、イベントハンドラ仮引数xdataydata 属性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 の中 の、ゲーム盤外の部分マウスを押す と、間違ったマスの座標変換される というものです。

以前の記事で説明したように、matplotlibAxesplot メソッドなどで 描画行った場合 は、Axes描画範囲 は、Axes登録 した すべての図形ぴったり収まる ような 範囲より少しだけ広い範囲 になるように 自動的設定 されます。〇×ゲームの ゲーム盤の画像 の場合は、Axesx 座標y 座標範囲 は、以前の記事確認 したように、 -0.15 ~ 3.15設定 されます。

下図5黄色部分 が、Axes の中 で、ゲーム盤外の部分 を表します。

0 への丸め座標の変換 を行った場合は、この 黄色い部分マウスを押した 際に、間違った座標表示 されます。黄色い部分 は、Axesx 座標 または 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)

実行結果(下図は、画像なので操作することはできません)

本記事では採用しませんが、この問題解決 する 別の方法 として、Axesx 座標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 の名前は、Axesin)に あるか どうかという 意味 です

  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