LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その70 マウスによる着手の処理

Last updated at Posted at 2024-04-07

目次と前回の記事

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

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

ルールベースの 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 行目マウス押した場所ゲーム盤の座標計算 し、xy代入 する
  • 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 行目guiTrue の場合draw_board記述 されていた Figure の作成設定イベントハンドラの定義結び付け の処理を そのまま記述 する

なお、draw_board は、play メソッド から self.draw_board によって 呼び出される ので、draw_boardself と、play メソッドの self には 同じ Marubatsu クラスの インスタンス代入 されています。そのため、draw_boardon_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 メソッドの axdraw_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 行目Axesy 軸反転 する処理です。

1  def draw_board(self, ax):             
2      # y 軸を反転させる
3      ax.invert_yaxis()
4    
5      # 枠と目盛りを表示しないようにする
6      ax.axis("off")
以下略

draw_board メソッドは、着手行うたび実行される ので、ax.invert_yaxis() によって、着手行うたびゲーム盤の画像上下反転する ことになります。

Axesy 軸反転する処理 は、以前の記事で説明したように、必要な処理 ですが、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 行目Axesy 軸反転する処理追加 する
  • 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 の情報削除 することで 解決 することができます。

別の言葉 で説明すると、ゲーム盤描画するたび に、それまで描画した内容すべて消して から、改めて ゲーム盤を 一から描画し直す ということです。

その処理 は、Axesclear メソッド2呼び出す ことで 行うことができる ので、その処理 を、下記 のプログラムのように、draw_board最初に記述 します。

  • 3 行目axclear メソッドを 実行 して、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)

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

Axesclear メソッドの詳細については、下記のリンク先を参照して下さい。

clear メソッドが行う処理

上記 のような 問題が発生 する 理由 は、Axes の clear メソッドが、Axes への Artist登録 の情報 だけでなく軸の反転 や、目盛りの非表示 など、Axes に対して行った すべての処理クリアしてしまう からです。

そのため、Axesclear メソッドを 実行した場合 は、下記 のプログラムの 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 メソッドは、FigureAxes両方 に対して 行うことができます が、ゲーム盤文字列 を表す Artist は、ax.plot()ax.text() によって 行う ことからわかるように、Axes に 対して 登録している ので、ゲーム盤の描画情報 を表す Artist削除する場合 は、Axesclear メソッドを 実行します

fig.clear()実行 することでも、Figure から、ゲーム盤Artist の情報削除 することが できます が、その場合 は、Figure登録 された Axes までも削除されてしまう3 ため、Axes再登録が必要 になる点に 注意 が必要です。

Figureclear メソッドの詳細については、下記のリンク先を参照して下さい。

決着後の着手の禁止

上記のプログラムの 問題 は、ゲーム決着後着手行えてしまう というものです。例えば、下図右 は、下図左勝利した後(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:説明一部間違っていた ので 修正 しました。

処理の流れ が良く わからなくなっている人いるかもしれません ので、guiFalse の場合に play メソッドを実行した際に行われる フロー駆動型プログラミングの処理 と、guiTrue の場合に 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人間どうしの対戦行える ようにしました。次回の記事 では、GUIAI と対戦 する 処理を記述 します。

本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

以下のリンクは、今回の記事で更新した marubatsu.py です。

次回の記事

更新日時 更新内容
2024/4/8 フローチャートの図と説明が一部間違っていたので修正しました
  1. イベントハンドラ に、決められた以外仮引数勝手に追加 すると エラーが発生 します

  2. cla というメソッドでも clear メソッドと 同じ処理 を行うことができます

  3. AxesFigure登録 された Artist の一種 だからです

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