LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その72 リセットボタンによるゲームのリセット

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 メソッドを 実行するたびFigure1 つ作成 されますが、作成された 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 行目guiTrue の場合に、上記の処理 を、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 AIAI 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