LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その73 重複する処理の統合

Last updated at Posted at 2024-04-18

目次と前回の記事

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

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

ルールベースの AI の一覧

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

処理の統合

前回の記事で、リセットボタン実装 しましたが、playon_button_clickedon_mouse_down似たような処理記述 されているので、統合 することにします。ただし、3 つの関数処理似てはいます が、同じプログラム記述されていない ので、統合するため には 3 つの関数 で行われる 処理を検証 する 必要あります

play メソッドと on_button_clicked の処理の統合

3 つの関数統合一度に行う のは 大変 なので、最初に play メソッドと on_button_clicked処理統合 を行うことにします。

似たような処理統合 するためには、具体的共通点調べる必要 があるので、play メソッドと on_button_clicked行う処理整理 することにします。

play メソッドが行う処理

play メソッドが行う 処理箇条書き にすると 以下 のようになります。

  1. 乱数の種関する処理 を行う
  2. ゲームリセット する
  3. Figure関する処理 を行う
  4. リセットボタン関する処理 を行う
  5. 繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
    1. ゲーム盤の画像描画 する
    2. 現在の手番AI の場合AI の着手選択 する
    3. 現在の手番人間 の場合 は、関数の処理終了 して イベントループ再開 する
  6. 繰り返しの処理終了後ゲーム盤の画像描画 する

言葉の説明だけ では わかりづらい ので、フローチャート説明 します。

play メソッドのフローチャートの整理

下図 は、以前の記事で紹介した play メソッドの フローチャート です。

GUI〇×ゲーム遊ぶ場合 は、guiTrue代入 されているので、その場合フローチャート下図 のようになります。なお、背景が 水色繰り返し処理 の、「AI の手番?」の 条件分岐 は、その 右上 の「人間の手番?」が no(False)の 場合実行される ので、必ず yes(True)になる点に 注意 して下さい。

上図 から、「guiTrue」の 条件分岐 と、実行されない部分削除 して 整理 すると、下図 のような フローチャート になります。なお、以前の記事フローチャート では、乱数の種 に関する 処理省略 されていたので、下図 には その処理追加 しました。また、以前の記事 でフローチャートを示した 時点 では、まだ リセットボタン に関する 処理実装されていなかった ので、下図 には リセットボタン に関する 処理追加 しました。

上図のフローチャートと、先程示した play メソッドが行う 処理見比べて みて下さい。

  1. 乱数の種関する処理 を行う
  2. ゲームリセット する
  3. Figure関する処理 を行う
  4. リセットボタン関する処理 を行う
  5. 繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
    1. ゲーム盤の画像描画 する
    2. 現在の手番AI の場合AI の着手選択 する
    3. 現在の手番人間 の場合 は、関数の処理終了 して イベントループ再開 する
  6. 繰り返しの処理終了後ゲーム盤の画像描画 する

on_mouse_clicked が行う処理

下記は、on_mouse_clicked の定義です。

        # リセットボタンのイベントハンドラを定義する
        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

上記の on_mouse_clicked 行う 処理箇条書き にすると 以下 のようになります。

  1. ゲームリセット する
  2. ゲーム盤の画像描画 する
  3. 繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
    1. 現在の手番AI の場合AI の着手選択 し、ゲーム盤の画像描画 する
    2. 現在の手番人間 の場合 は、関数の処理終了 して イベントループ再開 する

前回の記事で示したように、上記 の処理の フローチャート下図 のようになります。

play メソッドと on_button_clicked の比較

下記は、play メソッドと on_button_clicked処理の手順 です。それぞれを 見比べてどこ共通 し、どこが 異なるか について少し考えてみて下さい。

play メソッドの処理の手順

  1. 乱数の種関する処理 を行う
  2. ゲームリセット する
  3. Figure関する処理 を行う
  4. リセットボタン関する処理 を行う
  5. 繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
    1. ゲーム盤の画像描画 する
    2. 現在の手番AI の場合AI の着手選択 する
    3. 現在の手番人間 の場合 は、関数の処理終了 して イベントループ再開 する
  6. 繰り返しの処理終了後ゲーム盤の画像描画 する

on_button_clicked の処理の手順

  1. ゲームリセット する
  2. ゲーム盤の画像描画 する
  3. 繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
    1. 現在の手番AI の場合AI の着手選択 し、ゲーム盤の画像描画 する
    2. 現在の手番人間 の場合 は、関数の処理終了 して イベントループ再開 する

フローチャートの比較

わかりづらい と思った方は、下図の、play メソッドと on_button_clickedフローチャート を横に並べて 比較 してみると良いでしょう。行われる処理良く似ている ことが わかる のではないかと思います。なお、play メソッドの 処理の説明文一部 は、on_button_clicked表記合わせました

処理の共通点と違い

上記から、それぞれの処理 には 以下共通点 があることが わかります

  • ゲーム盤最初リセット する
  • 決着がつくまで繰り返し処理 を行う
  • 繰り返し 処理の で、AI の手番 の場合は AI着手を行う
  • 繰り返し 処理の で、人間の手番 の場合は 関数の処理終了 する

また、以下違い があることが わかります。ただし、GUI〇×ゲーム遊ぶ場合 は、play メソッドの 返り値利用しない ので、返り値の違い重要 では ありません

  • play メソッドは、乱数の種処理 と、Figureリセットボタン に関する 処理を行う
  • 画像描画 する タイミング異なる
  • play メソッドは 繰り返し処理終了時返り値 として 勝敗結果 を返す

上記の「画像描画 する タイミング異なる」という 違い から、一見すると 両者は 異なる処理 を行っているように 見えるかもしれません が、画像の描画 に関しては、play メソッドと on_button_clicked全く同じ処理 を行います。そのことを示します。

画像の描画の処理の検証

〇×ゲームゲーム盤の描画行う必要生じる のは、下記場合 です。

  • ゲーム開始した時
  • 着手行った時

また、GUI〇×ゲーム遊ぶ 際に、人間着手 を行った場合は、on_mouse_downイベントハンドラ内ゲーム盤の描画行います。そのため、play メソッドと on_button_clicked では、AI の着手 に対してのみ 画像の描画行われます

play メソッドと on_button_clicked は、上記 の処理を、下記タイミング行います。なお、下記の「最後の着手」とは、ゲーム決着がつく着手 の事を 表します

play on_button_clicked
ゲームを開始した時 最初繰り返し 処理 の 直後 繰り返し 処理の 直前
最後以外の AI の着手 繰り返し 処理の 直後 着手 を行った 直後
最後の AI の着手 繰り返し 処理の 終了後 着手 を行った 直後

わかりづらいと思いますので、フローチャート説明 します。

ゲームを開始した時の画像の描画のタイミング

下図ゲーム開始した時画像の描画 が行われる タイミング を表します。赤線画像描画されるまで に行われた 処理黄色の処理画像描画処理 を表します。

ゲーム開始時 の時点では 決着ついていない ので、いずれの場合 も、必ずゲーム盤描画 に関しては 同じ処理行われる ことが 確認 できます。

AI が最初に着手を行った時の画像の描画のタイミング

下図は、上記の後AI着手した場合画像の描画 が行われる タイミング を表します。水色の線 は、AI着手を選択 した 繰り返し 処理で 行われた処理 であることを表します。なお、AI の着手 は、最後の着手ではない ものとします。

からわかるように、play メソッドと on_button_clicked では、下記 のように 画像描画 する タイミング異なります が、いずれ着手行った後 で、 の「AI の手番?」の 判定行う前ゲーム盤の描画行われる ことが わかります

play on_button_clicked
繰り返し 処理の 直後 その回繰り返し 処理で 着手 を行った 直後

AI が 2 手目以降に着手を行った時の画像の描画のタイミング

下図は、AI2 手目以降着手した場合画像の描画 が行われる タイミング を表します。緑の線 は、AI着手を選択 した 繰り返し 処理で 行われた処理 であることを表します。なお、AI の着手 は、最後の着手ではない ものとします。

play メソッドの は、先程の図全く同じ です。また、on_button_clicked も、AI着手選択した直後ゲーム盤の画像描画 している点では 先程と同じ です。従って、先程と同様に、play メソッドと on_button_clicked では、下記 のタイミングで 画像描画 し、いずれ着手行った後 で、 の「AI の手番?」の 判定行う前ゲーム盤の描画行われる ことが わかります

play on_button_clicked
繰り返し 処理の 直後 その回繰り返し 処理で 着手 を行った 直後

人間の手番が回ってきた場合の処理

下図は、人間の手番回ってきた場合 に行われる 処理 です。人間の手番1 手目 の場合と 2 手目以降 の場合では、処理の流れ若干異なり ますが、本質的 には 変わらない ので、2 手目以降着手した場合画像の描画 が行われる タイミング を示します。

いずれの場合ゲーム盤の描画行わず関数の処理終了 することが わかります

AI が最後の着手を行った時の画像の描画のタイミング

下図は、AI最後の着手行った場合画像の描画 が行われる タイミング を表します。今回 の図は、画像の描画行われた後 で、関数処理が終了するまで流れ示しました線の色 は、処理が行われる ことを 表します

から、play メソッドは、繰り返し処理終了後ゲーム盤の画像描画 しますが、on_button_clicked では、これまでと同様 に、着手行った直後描画 します。いずれの場合関数の処理終了するまで の間に ゲーム盤描画 することが わかります

play on_button_clicked
繰り返し 処理が 終了 した 直後 その回繰り返し 処理で 着手 を行った 直後

まとめ

上記 から、いずれの場合 でも、play メソッドと on_button_clickedゲームの開始時 と、AI着手を行った後ゲーム盤画像を描画する という、同じ処理行う ことが 確認 できました。下記は、両者同じ処理を行う ことを 言葉で説明 したものです。

  • play メソッドは、繰り返し 処理の で、着手行う前画像を描画 する。そのため、ゲーム開始時描画処理繰り返し処理 の中で 行う ことが できる。一方、最後AI の着手 に対する 描画処理繰り返し 処理の 行えない ので、繰り返し 処理の 終了後その処理行う必要 がある
  • on_button_clicked は、繰り返し 処理の で、着手 を行った 直後画像を描画 する。そのため、着手 に対する 描画処理繰り返し処理 の中で 行う ことが できる。一方、ゲーム開始時描画処理繰り返し 処理の 行えない ので、繰り返し 処理の その処理行う必要 がある

play_common の定義

上記の考察 から、play メソッド と on_button_clicked行う処理違い は、play メソッドが 繰り返し 処理の 行う乱数の種関する処理」と「Figureリセットボタン関する処理だけ である事が わかりました

そこで、両者共通する処理 を行う メソッドを定義 することにします。その メソッドの名前 は、共通 するという 意味 を表す common という 英単語 を使って、play_common という 名前にする ことにします。

どちらの処理を play_common に記述すべきか

次に、play_common記述 する プログラム を、play メソッドと on_button_clickedどちらプログラム から 抜き出し記述 するかを 考える必要 があります。上記 で示したように、どちらも 同じ処理を行う のだから、どちらでも良い のではないかと 思う人いるかもしれません が、そうではありません。その理由について少し考えてみて下さい。

play メソッドは、guiFalse代入 されて 実行 される 場合あります。一方、on_button_clicked は、guiTrue代入 された場合に イベントハンドラ として 登録 されるため、guiTrue代入 されている場合 のみ実行 されます。そのため、play メソッドと on_button_clicked両方 から play_common メソッドを呼び出して 利用する場合 は、play メソッドの 内容を記述 しなければ、play メソッドで guiFalse代入 されている場合の処理に 対応できない という 問題が発生 します。

そこで、play_common を、play メソッドから 「乱数の種関する処理」と「Figureリセットボタン関する処理以外処理抜き出した下記 のプログラムのように 定義 します。play_common仮引数 は、とりあえず play メソッドと 同じものにする ことにしました。その 理由この後説明 します。

from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import japanize_matplotlib
import ipywidgets as widgets
import math

def play_common(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
    # 〇×ゲームを再起動する
    self.restart()

    # ゲームの決着がついていない間繰り返す
    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_common = play_common

play_common の不必要な仮引数の削除

上記 のプログラムを VSCode記述 すると、関数の定義 が、下図 のように 表示 されます。VSCode では、仮引数その関数ブロックの中 で一度も 利用されていない 場合は、薄い色の文字表示 されます。下図 では、seedsize薄い色で表示 されます。

従って、play_common仮引数 seedsize必要がない ことが わかります実際 に、VSCodeCtrl + f を押して表示される 検索機能 を使って、seedsizeplay_commonブロックの中 で一度も 記述されていない ことを 確認 して下さい。

下記 のプログラムは、仮引数 seedsize削除 した play_common定義 です。

def play_common(self, ai, params=[{}, {}], verbose=True, gui=False):
元と同じなので省略
    
Marubatsu.play_common = play_common
プログラム全体
def play_common(self, ai, params=[{}, {}], verbose=True, gui=False):
    # 〇×ゲームを再起動する
    self.restart()

    # ゲームの決着がついていない間繰り返す
    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_common = play_common
修正箇所
-def play_common(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
+def play_common(self, ai, params=[{}, {}], verbose=True, gui=False):
元と同じなので省略

Marubatsu.play_common = play_common

play メソッドと on_button_clicked の修正

次に、下記 のプログラムのように、play メソッドと on_button_clicked から、共通する処理削除 し、代わりplay_common呼び出す ように 修正 します。

  • 6 行目on_button_clicked に、play_common メソッドを 呼び出す処理のみ記述 する
  • 10 行目ゲーム決着がついていない間 繰り返す 処理を削除 し、return 文 で、play_common メソッドの 返り値返す処理記述する

play メソッドの場合は、勝敗結果返す 必要がある1ので、return 文play_common返り値返す必要がある 点に 注意 して下さい。イベントハンドラ返り値返す必要がない ので、on_button_clicked では、return 文記述する必要ありません

 1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
 2      # リセットボタンのイベントハンドラを定義する
 3      def on_button_clicked(b):
 4          # この下に記述されていた処理を削除する
 5   
 6          self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)           
元と同じなので省略         
 7
 8    # この下に記述されていた、ゲームの決着がついていない間繰り返す処理以降を削除する
 9  
10      return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
11    
12  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)
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)           
         
    # イベントハンドラをリセットボタンに結びつける
    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)     

    return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
    
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)
+       self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)           
元と同じなので省略         

    # ゲームの決着がついていない間繰り返す
-   while self.status == Marubatsu.PLAYING:
-   非常に長いのでこの間の部分は省略  
-   return self.status   
+   return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
    
Marubatsu.play = play

上記修正後 に、下記 のプログラムで play メソッドを 実行 すると、実行結果 のような、エラーが発生 します。このエラーの原因について少し考えてみて下さい。

%matplotlib widget
from ai import ai2
mb = Marubatsu()

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

実行結果(下記のエラーメッセージの下に表示される画像は省略します)

略
Cell In[3], line 45
     42     # fig の画像にマウスを押した際のイベントハンドラを結び付ける
     43     fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
---> 45 return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)

Cell In[2], line 49
     47 if verbose:
     48     if gui:
---> 49         self.draw_board(ax)
     50     else:
     51         print(self)

NameError: name 'ax' is not defined

エラーの検証と修正

エラーメッセージ から、ax定義されていない(not defined)ことが わかります。また、エラーが発生 している self.draw_board(ax) は、---> 45 return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui) という メッセージ から、play_common の中実行 されていることが わかります

従って、このエラー は、play_common の中 で、ax定義されていない ことが 原因 です。

上記名前が定義されていない という エラー は、以前の記事で説明したように、下図 のように、VSCodeplay_commonブロック内 のプログラムの ax の部分に オレンジ色波線表示 されることから 気づく ことも できます

そこで、下記 のプログラムのように、play_common仮引数 ax追加 します。なお、通常の仮引数 は、デフォルト引数より前記述 する 必要がある 点に 注意 して下さい。

def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
元と同じなので省略
    
Marubatsu.play_common = play_common
プログラム全体
def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
    # 〇×ゲームを再起動する
    self.restart()

    # ゲームの決着がついていない間繰り返す
    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_common = play_common
修正箇所
-def play_common(self, ai, params=[{}, {}], verbose=True, gui=False):
+def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
元と同じなので省略
    
Marubatsu.play_common = play_common

次に、下記 のプログラムの 4、7 行目 のように、play メソッドと on_button_clicked 内で play_common呼び出す際 に、実引数ax=ax記述 するように 修正 します。

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

    return self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
    
Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
-       self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)           
+       self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
         
元と同じなので省略 

-   return self.play_common(ai=ai, params=params, verbose=verbose, gui=gui)
+   return self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
    
Marubatsu.play = play

上記修正後 に、下記 のプログラムで play メソッドを 実行 し、正しく プログラムが 動作する ことを 確認 して下さい。また、人間 VS 人間AI VS 人間人間 VS AI でも 正しく プログラムが 動作する ことを 確認 して下さい。

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

play メソッドと on_mouse_down の処理の統合

次に、play メソッドと on_mouse_down処理統合 することにします。

play メソッドと on_mouse_down の処理の共通点

on_mouse_down行う処理箇条書き にすると 以下 のようになります。

  1. マウス押されたマス着手 を行う
  2. 着手後の ゲーム盤画像を描画 する
  3. ゲーム中手番AI の場合 は、AI の着手 を行い、画像を描画 する

そのように思えないかもしれませんが、上記手順 2、3処理 は、下記play メソッドの 手順 5、6置き換える ことが できます。その理由について少し考えてみて下さい。

  1. 乱数の種関する処理 を行う
  2. ゲームリセット する
  3. Figure関する処理 を行う
  4. リセットボタン関する処理 を行う
  5. 繰り返し処理 によって 決着がつくまで の間 以下の処理 を行う
    1. ゲーム盤の画像描画 する
    2. 現在の手番AI の場合AI の着手選択 する
    3. 現在の手番人間 の場合 は、関数の処理終了 して イベントループ再開 する
  6. 繰り返しの処理終了後ゲーム盤の画像描画 する

下図左on_mouse_down の、下図右on_mouse_down手順 2、3play メソッドの 手順 5、6置き換えフローチャート です。 の部分が on_mouse_down の、水色 の部分が play メソッドの 手順 であることを表します。このフローチャート を使って、両者同じ処理を行う ことを 示す ことにします。

on_mouse_down実行された ということは、×いずれか人間が担当 しているということなので、AI VS AI 以外組み合わせ の対戦で 行われる処理検証 します。

人間 VS 人間 の場合

on_mouse_down手順 1 は、以下2 種類の状況考えられる ので、それぞれ処理の流れ同じ であることを 示します

  • 手順 1 で行われた 着手 によって 決着つく
  • 手順 1 で行われた 着手 によって 決着つかない

下図 は、on_mouse_down手順 1 によって、ゲーム決着ついた場合処理 を表す フローチャート です。図から、いずれの場合手順 1 によって行われた 人間着手後ゲーム盤の描画 を行い、処理が終了 します。

下図 は、on_mouse_down手順 1 によって、ゲーム決着ついていない場合処理 を表す フローチャート です。下図左 で行われる 処理上記同じ です。下図右 で行われる 処理 は、処理の流れ変わっています が、人間着手後ゲーム盤の描画 を行い、処理が終了する という点では両者は 同じ処理行います

上記 から、人間 VS 人間 の場合は、両者は 同じ処理を行う ことが 確認 できました。

人間 VS AI または、AI VS 人間 の場合

人間 VS AIAI VS 人間いずれの場合 も、on_mouse_down手順 1人間の着手の処理 である点に 変わりはありません。そのため、両者処理の手順まとめて説明します

on_mouse_down手順 1 は、以下3 種類の状況考えられる ので、それぞれ処理の流れ同じ であることを 示します

  • 手順 1 で行われた 着手 によって 決着つく
  • 手順 1 で行われた 着手 によって 決着つかない
    • AI の着手 によって 決着つく
    • AI の着手 によって 決着つかない

手順 1 で行われた 着手 によって 決着つく 場合に 行われる処理 は、人間 VS 人間 の場合と 全く同じ なので説明は 省略 します。

下図 は、on_mouse_down手順 1 によって、ゲーム決着つかず、その AI の着手 によって 決着ついた場合処理 を表す フローチャート です。線の色 は、水色処理が行われる ことを 表します

図から、いずれの場合下記 の処理が 行われる ことが わかります

  • 人間着手後ゲーム盤の描画行われる
  • AI着手 を行い、ゲーム盤の描画行われる

下図 は、on_mouse_down手順 1 によって、ゲーム決着つかず、その AI の着手 によって 決着つかなかった場合処理 を表す フローチャート です。下図左 で行われる 処理上記同じ です。下図右 で行われる 処理 は、処理の流れ変わっています が、下記の処理行われる という点では、両者は 同じ処理行います。なお、下図右黄色ゲーム盤を描画する 処理は、人間着手後AI着手後2 度実行 されます。

  • 人間着手後ゲーム盤の描画行われる
  • AI着手 を行い、ゲーム盤の描画行われる

上記 から、人間 VS AIAI VS 人間 の場合も、同じ処理を行う ことが 確認 できました。

play_loop の定義

上記から、on_mouse_down手順 2、3play メソッドの 手順 5、6同じ処理行う ことが 確認 できました。そこで、play_common の場合と 同様 に、共通する処理 である play メソッドの 手順 5、6 の処理を 抜き出したメソッド定義 する事にします。

play メソッドの 手順 5処理 は、繰り返しの処理 なので、メソッド名前play_loop名付ける ことにし、下記 のプログラムのように 定義 します。

def play_loop(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
    # ゲームの決着がついていない間繰り返す
    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_loop = play_loop
play_commonとの違い
-def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
+def play_loop(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
    # 〇×ゲームを再起動する
-   self.restart()
元と同じなので省略
    
Marubatsu.play_loop = play_loop

on_mouse_down の修正

次に、on_mouse_down の、マウス押したマス着手を行った後処理 を、下記 のプログラムのように、play_loop呼び出す プログラムに 修正 します。

  • 12 行目AI着手を行う処理削除 し、play_loop呼び出す 処理を 記述 する
 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                  # 次の手番の処理を行うメソッドを呼び出す
12                  self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
13                   
元と同じなので省略
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)
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
         
    # イベントハンドラをリセットボタンに結びつける
    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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                   
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    return self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
    
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 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)                   
                # 次の手番の処理を行うメソッドを呼び出す
+               self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                   
元と同じなので省略

Marubatsu.play = play

上記修正後 に、下記 のプログラムで play メソッドを 実行 し、正しく プログラムが 動作する ことを 確認 して下さい。また、人間 VS 人間AI VS 人間人間 VS AI でも 正しく プログラムが 動作する ことを 確認 して下さい。

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

play_commonplay_loop の統合

ここまでで、playon_button_clickedon_mouse_down処理統合行うため に、play_loopplay_common2 種類メソッド定義 しましたが、この 2 つのメソッド で行われる 処理の違い は下記の点だけなので、統合 したほうが良いでしょう。

  • 最初self.restart()実行 して ゲームリセットするか どうか

従って、play_common の中で、play メソッドの 手順 5、6 の処理を 下記 のプログラムの 6 行目 のように play_loop呼び出す ように 修正 することが できます

 1  def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4
 5      # 決着がつくまでの繰り返し処理を行うメソッドを呼び出す
 6      return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
 7
 8  Marubatsu.play_common = play_common
行番号のないプログラム
def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
    # 〇×ゲームを再起動する
    self.restart()

    # 決着がつくまでの繰り返し処理を行うメソッドを呼び出す
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play_common = play_common
修正箇所
def play_common(self, ai, ax, params=[{}, {}], verbose=True, gui=False):
    # 〇×ゲームを再起動する
    self.restart()

    # 決着がつくまでの繰り返し処理を行うメソッドを呼び出す
+   self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
-   # ゲームの決着がついていない間繰り返す
-   while self.status == Marubatsu.PLAYING:
-   以下非常に長いので省略します

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            self.draw_board(ax)
        else:
            print(self)
            
    return self.status
    
Marubatsu.play_common = play_common

上記修正後 に、下記 のプログラムで play メソッドを 実行 し、正しく プログラムが 動作する ことを 確認 して下さい。また、人間 VS 人間AI VS 人間人間 VS AI でも 正しく プログラムが 動作する ことを 確認 して下さい。

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

play_common の削除

play_common行う処理 は、self.restartplay_loop呼び出す という 処理だけ なので、わざわざ メソッドとして定義 する 必要はない思う人いるかもしれません

そのように感じた人は、下記 のプログラムの 5 ~ 7、9 ~ 11 行目 のように、play メソッドと on_button_clicked 内で play_common呼び出す処理 を、self.restartplay_loop を呼び出す処理に 修正 すると良いでしょう。

 1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
 2      # リセットボタンのイベントハンドラを定義する
 3      def on_button_clicked(b):
 4          # ゲームをリセットする
 5          self.restart()
 6          # 決着がつくまで繰り返す処理を呼び出す
 7          self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
元と同じなので省略
 8      # ゲームをリセットする
 9      self.restart()
10      # 決着がつくまで繰り返す処理を呼び出す
11      return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
12    
13  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)
    
    # リセットボタンを配置する
    button = widgets.Button(description="リセット")
    display(button)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        # ゲームをリセットする
        self.restart()
        # 決着がつくまで繰り返す処理を呼び出す
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
         
    # イベントハンドラをリセットボタンに結びつける
    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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                   
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    # ゲームをリセットする
    self.restart()
    # 決着がつくまで繰り返す処理を呼び出す
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
    
Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        # ゲームをリセットする
+       self.restart()
        # 決着がつくまで繰り返す処理を呼び出す
-       self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
+       self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
元と同じなので省略
    # ゲームをリセットする
+   self.restart()
    # 決着がつくまで繰り返す処理を呼び出す
-   return self.play_common(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
+   return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
    
Marubatsu.play = play

本記事 では 上記 のプログラムを 採用 しますが、修正前修正後 のプログラムで、どちらが良いか については 一概には言えませんわかりやすい思ったほう採用して下さい。なお、play_common はもう 利用しない ので、marubatsu.py には 記述しません

上記修正後 に、下記 のプログラムで play メソッドを 実行 し、正しく プログラムが 動作する ことを 確認 して下さい。また、人間 VS 人間AI VS 人間人間 VS AI でも 正しく プログラムが 動作する ことを 確認 して下さい。

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

今回の記事のまとめ

今回の記事では、playon_button_clickedon_mouse_down処理統合 を行いました。このような 処理の統合行わなくてもリセットボタン処理を記述 することが できます が、統合したほう が、プログラム簡潔に記述できる ようになります。統合前統合後 のプログラムを 見比べて、そのことを 確認 してみて下さい。

また、同じ処理 をプログラムの 複数の個所記述する ことは、避けたほうが良い ので、このような 統合ができる場合 は、しておいたほうが良い でしょう。

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

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

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

次回の記事

  1. play メソッドの 返り値 は、ai_match を使って 通算成績計算 する際などで 必要 になります

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