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を一から作成する その72 リセットボタンによるゲームのリセット

0
Last updated at Posted at 2024-04-14

目次と前回の記事

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

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

ルールベースの AI の一覧

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

ボタンによるゲームのリセット

前回の記事で、〇×ゲーム を GUI で遊べる ようになりました。今回の記事では、ipywidgets を利用して、ゲーム を リセットする ための ボタン を 実装 することにします。

リセットボタンの必要性

これまで の記事では、新しいゲーム を 開始 するたびに、play メソッドを 実行 してきましたが、その際 に 下記 の メッセージが表示 されたことはないでしょうか。なお、実際のメッセージ は 1 行 で表示されますが、わかりやすいよう に 下記 では 改行 を行っています。

C:\Users\ys\AppData\Local\Temp\ipykernel_1672\1311249836.py:6: RuntimeWarning: 
More than 20 figures have been opened. Figures created through the pyplot interface
(`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume
too much memory. (To control this warning, see the rcParam `figure.max_open_warning`).
Consider using `matplotlib.pyplot.close()`.
  plt.subplots(figsize=[0.1, 0.1])

上記 のメッセージの中の、下記 のメッセージの 意味 は、以下の通り です。

More than 20 figures have been opened.
Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are
retained until explicitly closed and may consume too much memory.

20 より多く(more than 20)の 画像(figures)が 開かれている(has been opened)。
pyplot の(pyplot.figure メソッドなどの)インターフェース1(interface)よって 作成 された(created through)Figure は、明示的 に(explicitly)閉じられる(closed)まで(until)保持される(retained)ため、多く(too many)の メモリ(memory)を 消費(consume)する 可能性(may)がある。

以前の記事で説明したように、%matplotlib widget を 実行 した場合は、作成 された Figure が、pyplot が 管理 する Figure の一覧から 自動的 に__削除__ されることは ありません。play メソッドは、その中 で subplots メソッドによって 新しい Figure を 作成する ため、play メソッドを 実行するたび に Figure が 1 つ作成 されますが、作成された Figure は、すべて残り続ける ために 上記 のような 警告のメッセージ が 表示 されます。

上記 の メッセージ を 自分の目 で 確認したい 人は、下記の プログラムを 実行 してみて下さい。下記 のプログラムは、plt.subplots によって、21 個 の Figure を 作成 するプログラムで、実行 すると 上記のメッセージ と、21 個 の 画像 が 描画 されます。なお、描画 される Figure の 画像 を 小さくするため に、figsize=[0.1, 0.1] を 実引数に記述 しました。実行結果は長いので省略します。

%matplotlib widget
import matplotlib.pyplot as plt
import japanize_matplotlib

for i in range(21):
    plt.subplots(figsize=[0.1, 0.1])

プログラム の バグ を 修正した際 に、動作を確認 するために play メソッドを 何度も実行 することは 仕方がありません が、〇×ゲーム の 新しいゲーム を 遊ぶため に play メソッドを 毎回実行 すると、実行した回数 だけ Figure が 作成される ことになるため、その分だけコンピューターの メモリ が 消費されてしまう ことになります。そこで、ipywidgets の ボタン の クリック で 〇×ゲーム を リセットできる ようにすることで、play メソッドを 実行しなくても、新しいゲームを始める ことが できるよう にします。

ボタンの作成とイベントハンドラの登録のおさらい

ipywidgets の ボタン は、以前の記事で説明した方法で、下記 のプログラムで作成し、JupyterLab に表示 することが できます。

import ipywidgets as widgets

button = widgets.Button(description="Click me")
display(button)

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

作成 した ボタン を クリック した際に 実行 する イベントハンドラ は、以前の記事で説明した方法で、下記 のプログラムのように 定義 して、ボタン に 結びつける ことが できます。実行結果は、上記のプログラムを実行した後で、ボタンを 3 回クリックした場合の図です。

def on_button_clicked(b):
    print("ボタンがおされたよ!")

button.on_click(on_button_clicked)

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

〇×ゲームのリセットボタンの表示

〇×ゲーム の リセットボタン は play メソッドを 実行 した際に 表示 するので、下記 のプログラムのように、上記の処理 を、play メソッドで Figure を 作成する前 に 記述 します。

  • 8 ~ 16 行目:gui が True の場合に、上記の処理 を、play メソッドの中に 記述 する
 1  from marubatsu import Marubatsu
 2  import math
 3
 4  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
 5      # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
 6      if gui:
 7          # リセットボタンを配置する
 8          button = widgets.Button(description="リセット")
 9          display(button)
10        
11          # リセットボタンのイベントハンドラを定義する
12          def on_button_clicked(b):
13              print("ボタンがおされたよ!")    
14              
15          # イベントハンドラをリセットボタンに結びつける
16          button.on_click(on_button_clicked)
17
18          fig, ax = plt.subplots(figsize=[size, size])
元と同じなので省略        
19
20  Marubatsu.play = play
行番号のないプログラム
from marubatsu import Marubatsu
import math

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:
        # リセットボタンを配置する
        button = widgets.Button(description="リセット")
        display(button)
        
        # リセットボタンのイベントハンドラを定義する
        def on_button_clicked(b):
            print("ボタンがおされたよ!")    
            
        # イベントハンドラをリセットボタンに結びつける
        button.on_click(on_button_clicked)

        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:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
        # 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
修正箇所
from marubatsu import Marubatsu
import math

def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        # リセットボタンを配置する
+       button = widgets.Button(description="リセット")
+       display(button)
        
        # リセットボタンのイベントハンドラを定義する
+       def on_button_clicked(b):
+           print("ボタンがおされたよ!")    
            
        # イベントハンドラをリセットボタンに結びつける
+       button.on_click(on_button_clicked)

        fig, ax = plt.subplots(figsize=[size, size])
元と同じなので省略        

Marubatsu.play = play

上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 すると、実行結果 のように、リセットボタン が ゲーム盤の上 に 表示 されるようになり、リセットボタン を クリックするたび に、実行結果 のように メッセージ が 表示される ようになります。実際 に下記のプログラムを実行して 確認 してみて下さい。なお、下記 は 人間どうし の 対戦 です。

mb = Marubatsu()
mb.play(ai=[None, None], gui=True)

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

実は、ボタン に 関する処理 を、Figure を 作成する処理 の 後に記述 しても、実行結果 は 変化せず、ボタン は ゲーム盤 の 画像より上 に 表示 されます。ウィジェット の 表示 の レイアウト を 変える方法 については 今後の記事 で 紹介 します。

リセットボタンのクリックによるゲームのリセットの処理

リセットボタン を クリック した時の 処理 は、Marubatsu クラスの restart メソッドによって 行う ことが できます。また、上記 のプログラムでは、on_button_clicked の イベントハンドラ を、play メソッドの ローカル関数 として 定義 したので、on_mouse_down と 同様 に、Marubatsu クラスの インスタンス が 代入 された play メソッドの 仮引数 self を そのまま利用 することが できます。従って、on_button_clicked を 下記 のように 修正 する事で、リセットボタン を クリック すると 〇×ゲーム を リセットできる ようになります。

  • 4 行目:リセットボタン が 押された場合 に呼び出される on_button_clicked で、restart メソッドを呼び出して ゲーム を リセットする
1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略  
2      # リセットボタンのイベントハンドラを定義する
3      def on_button_clicked(b):
4          self.restart()     
元と同じなので省略
5    
6  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()
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()     
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # 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)

                # 現在の手番を表す ai のインデックスを計算する
                index = 0 if self.turn == Marubatsu.CIRCLE else 1
                # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
                if self.status == Marubatsu.PLAYING and ai[index] is not None:                
                    x, y = ai[index](self, **params[index])
                    self.move(x, y) 
                    self.draw_board(ax)
                   
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # 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_button_clicked(b):
-       print("ボタンがおされたよ!") 
+       self.restart()     
元と同じなので省略
    
Marubatsu.play = play

上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 し、いくつか のマスに 着手 を 行った後 で、リセットボタン を クリック してみて下さい。

mb.play(ai=[None, None], gui=True)

残念ながら リセットボタン を クリック しても 画面の表示 は 変化しません。しかし、その後 で マスの上 で マウスを押す と、押したマス以外 の マーク が リセット されるという、一見 すると 不思議なこと がおきます。下図右 は、下図左 の 局面 で リセットボタン を クリックした後 で、(1, 1) の マスの上 で マウスを押した 場合です。何故このようなことが起きるかについて少し考えてみて下さい。

 

問題の検証と修正

上記の問題 は、restart メソッドによって 〇×ゲーム を リセットした後 で、ゲーム盤の描画 を 更新していない ことが 原因 です。

これまでのプログラム で 着手を行った後 に ゲーム盤の描画 が 更新 されていたのは、下記 のプログラムの 8 行目 のように、move メソッドで 着手 を 行った後 で、draw_board メソッドを 実行 して ゲーム盤の画像 を 更新する処理 を行っていたからです。

1  # ローカル関数としてイベントハンドラを定義する
2  def on_mouse_down(event):
3      # Axes の上でマウスを押していた場合で、ゲーム中の場合のみ処理を行う
4      if event.inaxes and self.status == Marubatsu.PLAYING:
5          x = math.floor(event.xdata)
6          y = math.floor(event.ydata)
7          self.move(x, y)                
8          self.draw_board(ax)
以下略

初心者の方でコンピュータは賢いはずだから、ゲーム を リセット したら 気を利かせて画面の描画 を 更新してくれる だろうと 考える人 が いるかもしれません が、プログラム は、記述 した事 しか行わない 点に 注意 して下さい。ゲーム盤 の 状況が変化 した際に、このような 描画の更新 の し忘れ は よくあるミス なので紹介しました。

下記 のプログラムの 5 行目 のように、restart メソッドを 呼び出した後 で、draw_board メソッドを 呼び出す ように 修正 することで、この問題 を 解決 することが できます。

1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2      # リセットボタンのイベントハンドラを定義する
3      def on_button_clicked(b):
4          self.restart()       
5          self.draw_board(ax)       
6
元と同じなので省略         
7    
8  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()
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
        self.draw_board(ax)       
         
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # 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)

                # 現在の手番を表す ai のインデックスを計算する
                index = 0 if self.turn == Marubatsu.CIRCLE else 1
                # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
                if self.status == Marubatsu.PLAYING and ai[index] is not None:                
                    x, y = ai[index](self, **params[index])
                    self.move(x, y) 
                    self.draw_board(ax)
                   
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # 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_button_clicked(b):
        self.restart()       
+       self.draw_board(ax)       

元と同じなので省略         
    
Marubatsu.play = play

下図は、修正後 の on_button_clicked のフローチャートです。

上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 し、いくつかのマス に 着手 を 行った後 で、リセットボタン を クリック することで、問題 が 解決 されたことを 確認 して下さい。なお、実行結果は省略します。

mb.play(ai=[None, None], gui=True)

上記の修正 で、リセットボタン の 処理 が 実装 できた 思う人 が いるかもしれません が、実は 上記 のプログラムには 問題があります。それが何かを少し考えてみて下さい。

リセットボタンの処理の問題点

上記 のプログラムの 問題点 は、先手 である 〇 を AI が担当 した際に、リセット後 に AI が 着手を行わない というものです。下記 の ai2 VS ai2 のプログラムを 実行 すると、実行結果 のように、AI どうし の 対戦結果 が 表示 されます。この 処理 には 問題 は ありません。

from ai import ai2

mb.play(ai=[ai2, ai2], gui=True)

実行結果(実行結果はランダムなので下記とは異なる場合があります)

下図 は、上記 の 実行結果 で、リセットボタン を クリック した場合の 図 です。図 のように、AI どうし の対戦であるにも 関わらず、着手 が 行われない ことが 確認 できます。

また、上図 の ゲーム盤の上 で マウスを押す と、そのマスに 着手 が 行われた後 で、× を担当 する AI が 着手 を 行います。下図 は、上図 で (1, 1) に 着手 を 行った場合 の図です。図のように、× を担当 する AI が (2, 2) のマスに 着手 を 行います。なお、ai2 は ランダムな着手 を行うので、下図 とは 異なるマス に 着手 が行われる 場合 が あります。

以後 も、ゲーム盤の上 で マウス を押して 〇 の着手 を 行う と、× を担当 する AI が 着手 を 行います。このように、最初 は AI どうし の 対戦 を行っていたにも 関わらず、リセットボタン を クリック すると、人間 VS AI の 対戦 が 行われる という 問題が発生 しています。

上記 の 問題点 を まとめる と 以下 のようになります。

  • AI VS AI の 対戦 を行った場合、リセットボタン の クリック後 に AI が着手 を 行わない
  • ゲーム盤の上 で マウスを押す と、人間 が 〇 の 着手 を 行えてしまう
  • 人間 が 〇 の着手 を 行った後 で、AI が × の着手 を 行う
  • その後 も、人間 が 〇 の着手 を 行えてしまう
  • 結果として、人間 VS AI の 対戦 が 行われてしまう

何故このようなことが起きるかについて少し考えてみて下さい。

リセットボタンの下に表示される文字列の削除

上記の問題の原因を説明する前に、リセットボタン の 下に表示 される 文字列 について 説明 します。リセットボタン の 下に表示 される 'x' は、× が勝利 したことを表す play メソッドの 返り値 です。以前の記事のノートで説明したように、JupyterLab では、セルの最後 に 記述 された 文 の 計算結果 が 表示される ようになっているので、play メソッドの 返り値 が 下図 のように リセットボタンの下 に 表示 されます。

play メソッドの 返り値 を 表示したくない 場合は、下記 のプログラムのように、セル の 最後の文 の 後 に ; を記述 します。; は、文を区切る ための 記号 で、; の後 に 何も記述しない ことで、セルの最後 に 記述 された 文 が 空の文 に なります。セルの最後 で 空の文 を 実行 した場合は 何も表示されない ので、下記 のプログラムのように ; の前 に 記述 した play メソッドの 計算結果 が 表示されなくなります。

mb.play(ai=[ai2, ai2], gui=True);

実行結果(実行結果はランダムなので下記とは異なる場合があります)

以後 は、余計な表示 を 行いたくない 場合に、セル の 最後の文の後 に ; を記述 します。

実は、play メソッドの 返り値など は、これまでのプログラム でも 表示されていました が、あまり 目立たなかった ので、本記事の 実行結果 に 表記しません でした。

リセットボタン を 表示 した場合は、リセットボタンの下 に 表示される ため、目立ってしまう ので、上記の方法を使って 表示しないようにする ことにしました。

リセットボタンのクリック後に AI が着手を行わない問題の原因

下記 は、リセットボタン を クリック した際に 実行 される イベントハンドラ です。

    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
        self.draw_board(ax)   

この イベントハンドラ の 処理 は、「ゲーム の リセット」と「ゲーム盤 の 描画の処理」を行うと 終了 します。そのため、リセットボタン を クリック した 後 に、AI が 〇 を担当 していた場合でも、AI が 着手 の 処理を行う ことは ありません。

人間が 〇 の着手を行えてしまう問題の原因

リセットボタン を クリック した場合に 実行 される イベントハンドラ の 処理が終了 すると、イベントループ の 処理が再開 されます。そのため、マウスを押す と on_mouse_down の イベントハンドラ が 実行 され、マウス を 押したマス に 着手を行う 処理が 実行 されます。これが、人間 が 〇 の着手 を 行えてしまう問題 の 原因 です。

人間が 〇 の着手を行った後で AI が × の着手を行う原因

ゲーム盤の上 で マウスを押す と、下記 の イベントハンドラ が 実行 されます。

 1  def on_mouse_down(event):
 2      # Axes の上でマウスを押していた場合のみ処理を行う
 3      if event.inaxes and self.status == Marubatsu.PLAYING:
 4          x = math.floor(event.xdata)
 5          y = math.floor(event.ydata)
 6          self.move(x, y)                
 7          self.draw_board(ax)
 8
 9          # 現在の手番を表す ai のインデックスを計算する
10          index = 0 if self.turn == Marubatsu.CIRCLE else 1
11          # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
12          if self.status == Marubatsu.PLAYING and ai[index] is not None:                
13              x, y = ai[index](self, **params[index])
14              self.move(x, y) 
15              self.draw_board(ax)

この イベントハンドラ は、マウスが押されたマス に 着手 を 行った後 で 次の手番 を AI が担当 していた場合は、12 ~ 15 行目 で、AI が 着手を行う処理 を 行います。先程 play メソッドで 開始 したゲームは、ai2 VS ai2 だったので、6 行目 の 〇 の 着手後 に × を担当 する AI が着手 を行い、この イベントハンドラの処理 は 終了 します。これが、人間 が 〇 の着手 を 行った後 で × を担当 する AI が 着手を行う原因 です。

その後も人間が 〇 の着手を行えてしまう原因

上記の処理 を 行った後 で、AI が 着手を行う処理 は 記述されていない ので、次 の 〇 の着手 を AI が行う ことは ありません。また、on_mouse_down の 処理 は 終了する ので、イベントループ の 処理が再開 します。そのため、先程 と 同じ理由 で、その後 も 人間 が 〇 の着手 を 行えてしまいます。

結果として 人間 VS AI の 対戦が行われてしまう問題の原因

上記から、人間 が 〇 の着手 を 行うたび に × を担当 する AI が 着手 を行い、再び 人間 が 〇 の着手 を 行うことができる ようになります。そのため、結果 として 人間 VS AI の 対戦 が行われます。以上 が 問題の原因 です。

処理のフローチャート

下図は、リセットボタン を 押した場合 に呼び出される on_button_clicked の 処理 の フローチャート です。図から、AI が 着手を行わない ことが わかります。

下図は、その後 に ゲーム盤の上 で マウス を 押すたび に 呼び出される on_mouse_down の 処理 の フローチャート です。実際に行われる 赤線の処理 からわかるように、人間 の 〇 の着手 が 行われた後 で、AI が × の着手 を行い、処理が終了 します。

問題の原因 が わかりました ので、問題 を 解決する方法 について少し考えてみて下さい。

問題の解決方法

初心者 の方は、下記の 間違った 問題の 解決方法 を 思いつく人 が 多いのではないか と思いますので、間違った 問題の 解決方法 を 紹介 してから、正しい解決方法 を 説明 します。

間違った問題の解決方法

上記 の現象は、前回の記事で、人間 VS AI の 処理 を 実装する際 に 起きた、人間 が 着手を行った後 に AI が 着手を行わない という現象と 似ています です。その際には、on_mouse_down が 着手の処理 を 行った後 で、「次の手番 を AI が担当 する場合は その AI が 着手を選択 する」という 処理を記述 することで 問題を解決 しました。

下記は、on_mouse_down の その部分の処理 を 緑色で囲った フローチャートです。

そのため、同様の考え方 で、リセットボタン を クリック した時の イベントハンドラ に、下記 のプログラムのように ゲーム の リセットの処理 を 行った後 で「AI の手番 の場合は、その AI が 着手を選択 する」処理を記述 すればよいと 考えた人 はいないでしょうか?

  • 8 ~ 13 行目:リセットボタン を クリック した場合の イベントハンドラ の 最後 に、ゲーム盤の上 で マウス を 押した場合 の on_mouse_down の イベントハンドラ の 中 の、現在の手番 が AI の場合 に AI が 着手を行う処理 と 同じ処理 を そのまま記述 する
 1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略  
 2      # リセットボタンのイベントハンドラを定義する
 3      def on_button_clicked(b):
 4          self.restart()       
 5          self.draw_board(ax)  
 6       
 7          # 現在の手番を表す ai のインデックスを計算する
 8          index = 0 if self.turn == Marubatsu.CIRCLE else 1
 9          # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
10          if self.status == Marubatsu.PLAYING and ai[index] is not None:                
11              x, y = ai[index](self, **params[index])
12              self.move(x, y) 
13              self.draw_board(ax)                               
元と同じなので省略
14    
15  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()
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
        self.draw_board(ax)  
        
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
        if self.status == Marubatsu.PLAYING and ai[index] is not None:                
            x, y = ai[index](self, **params[index])
            self.move(x, y) 
            self.draw_board(ax)                               
         
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # 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)

                # 現在の手番を表す ai のインデックスを計算する
                index = 0 if self.turn == Marubatsu.CIRCLE else 1
                # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
                if self.status == Marubatsu.PLAYING and ai[index] is not None:                
                    x, y = ai[index](self, **params[index])
                    self.move(x, y) 
                    self.draw_board(ax)
                   
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # 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_button_clicked(b):
        self.restart()       
        self.draw_board(ax)  
        
        # 現在の手番を表す ai のインデックスを計算する
+       index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
+       if self.status == Marubatsu.PLAYING and ai[index] is not None:                
+           x, y = ai[index](self, **params[index])
+           self.move(x, y) 
+           self.draw_board(ax)                               
元と同じなので省略
    
Marubatsu.play = play

下図左 は、on_button_clicked の 追加した部分 を 緑色で囲った フローチャートです。下図右 の on_mouse_down のフローチャートと 見比べて みて下さい。

 

上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 すると、先程と同様に、AI どうし の 試合結果 が 表示 されます。

mb.play(ai=[ai2, ai2], gui=True);

実行結果(実行結果はランダムなので下記とは異なる場合があります)

上図 で リセットボタン を クリック すると、今度 は 〇 の手番 を 担当する AI が 着手を行い、下図 のような 画像 が 表示 されます。しかし、下図 からわかるように、その次 の × を担当 する AI が 着手を行わない という 問題が発生 します。なお、ai2 はランダムな着手を行うので下図とは異なる場合があります

図は示しませんが、上図 で ゲーム盤 の 画像の上 で マウスを押す と、× の着手 が 行われた後 で、AI が 〇 の着手 を 行います。従って、この場合 は、AI VS 人間 の 対戦 が 行われる ことになります。何故このようなことが起きるかについて少し考えてみて下さい。

問題の検証と修正

下図は、リセットボタン を 押した場合 に呼び出される on_button_clicked の 処理 の フローチャート です。図から、AI が 〇 の着手のみ を 行う ことが わかります。

下図は、その後 に ゲーム盤の上 で マウス を 押すたび に 呼び出される on_mouse_down の 処理 の フローチャート です。実際に行われる 赤線の処理 からわかるように、人間 の × の着手 が 行われた後 で、AI が 〇 の着手 を行い、処理が終了 します。なお、下図 は、先程の 人間 VS AI の 場合 に、on_mouse_down が 呼び出され た際に 行われる処理 と 同じ です。

 

この問題は、リセットボタン を クリック した際の イベントハンドラ である on_button_clicked が、AI の着手 を 行う処理 を 1 度しか行わない 点にあります。AI どうし の 対戦 の場合は、下記 の フローチャート のように、1 度ではなく、ゲーム の 決着がつくまで 繰り返し AI の着手 を 行う必要 が あります。

下図左 は、AI どうし の 対戦 を行った場合の on_button_clicked の 処理 を 赤線 と 紫色 の 線 で示した フローチャート です。赤色 の線は、決着がついていない 場合、紫色 の線は 決着がついた 場合の 処理の流れ です。下図右 の、修正前 の on_button_clicked の フローチャート と 見比べて みて下さい。

 

下記 は 上記 の フローチャート のように on_button_clicked を 修正 したプログラムです。

  • 7 行目:ゲーム の 決着がつくまで 繰り返し 処理を行う
  • 11 ~ 14 行目:AI の手番 の場合は、AI が 着手を選択 する。なお、元 のプログラムの 11 行目 の 条件式 には、self.status == Marubatsu.PLAYING が 記述 されていたが、その条件式 が 満たされること は、7 行目 の while 文 の 条件式 で 保証されている ので、11 行目 の 条件式から削除 した
  • 17 行目:人間の手番 の場合は、return 文を 記述 して、イベントハンドラ の 処理を終了 することで、イベントループを再開 して マウス による 着手 が 行えるようにする
 1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
 2      # リセットボタンのイベントハンドラを定義する
 3      def on_button_clicked(b):
 4          self.restart()       
 5          self.draw_board(ax)  
 6       
 7          while self.status == Marubatsu.PLAYING:
 8              # 現在の手番を表す ai のインデックスを計算する
 9              index = 0 if self.turn == Marubatsu.CIRCLE else 1
10              # ai が着手を行うかどうかを判定する
11              if ai[index] is not None:                
12                  x, y = ai[index](self, **params[index])
13                  self.move(x, y) 
14                  self.draw_board(ax)                               
15              else:
16                  # 人間の手番の場合は、イベントハンドラを終了する
17                  return
元と同じなので省略         
18    
19  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()
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()       
        self.draw_board(ax)  
        
        while self.status == Marubatsu.PLAYING:
            # 現在の手番を表す ai のインデックスを計算する
            index = 0 if self.turn == Marubatsu.CIRCLE else 1
            # ai が着手を行うかどうかを判定する
            if ai[index] is not None:                
                x, y = ai[index](self, **params[index])
                self.move(x, y) 
                self.draw_board(ax)                               
            else:
                # 人間の手番の場合は、イベントハンドラを終了する
                return
         
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # 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)

                # 現在の手番を表す ai のインデックスを計算する
                index = 0 if self.turn == Marubatsu.CIRCLE else 1
                # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
                if self.status == Marubatsu.PLAYING and ai[index] is not None:                
                    x, y = ai[index](self, **params[index])
                    self.move(x, y) 
                    self.draw_board(ax)
                   
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # 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_button_clicked(b):
        self.restart()       
        self.draw_board(ax)  
        
+       while self.status == Marubatsu.PLAYING:
            # 現在の手番を表す ai のインデックスを計算する
            index = 0 if self.turn == Marubatsu.CIRCLE else 1
            # ai が着手を行うかどうかを判定する
-           if self.status == Marubatsu.PLAYIN and ai[index] is not None:                
+           if ai[index] is not None:                
                x, y = ai[index](self, **params[index])
                self.move(x, y) 
                self.draw_board(ax)                               
+           else:
                # 人間の手番の場合は、イベントハンドラを終了する
+               return
元と同じなので省略         
    
Marubatsu.play = play

実行結果は省略しますが、上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 すると、今度は リセットボタン を クリック する たび に AI どうし の 対戦 が 行われる ことが 確認 できます。本記事では省略しますが、人間 VS 人間、人間 VS AI、AI VS 人間 の 対戦 でも 正しく動作 することを、確認 してください。

mb.play(ai=[ai2, ai2], gui=True);

今回の記事のまとめ

今回の記事では、ipywidgets の ボタン を 利用 することで、リセットボタン を 表示 して、〇×ゲーム の リセット を行うことが できるように しました。

ただし、今回の記事 で 記述 した プログラム には、同じような内容 のプログラムが 複数の個所 で 重複 しているという 問題がある ので、次回の記事では その問題 を 修正 します。

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

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

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

次回の記事

  1. ここでいう インターフェース は、ユーザーインターフェースの場合と同様に 操作環境 の事を表します。具体的には pyplot に関する 操作を行う ための メソッド のことを表します ↩

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?