Pythonで〇×ゲームのAIを一から作成する　その71　GUI による AI との対戦

# 目次と前回の記事

https://qiita.com/ysgeso/items/2381dd4e3283cbed49a0

https://qiita.com/ysgeso/items/c91d5373753253d98f23

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

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

https://github.com/ysgeso/marubatsu/blob/master/071/marubatsu.py

https://github.com/ysgeso/marubatsu/blob/master/071/ai.py

## ルールベースの AI の一覧

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

https://qiita.com/ysgeso/items/10d1d01192c014173e4b

# GUI での AI との対戦

前回の記事では、__人間どうし__ が __GUI__ で __〇×ゲーム__ を __遊べる__ ようにしました。

今回の記事では __GUI__ で __AI の対戦__ が __できるよう__ にします。

__AI の対戦__ は、下記の __3 種類__ があります。なお、__下記__ の表記は、__VS__ の __前に記述__ したものが __〇 を担当__ するという __意味__ を表します。__それぞれ__ について __順番に実装__ を行います。

* __AI__ VS __AI__
* __人間__ VS __AI__
* __AI__ VS __人間__

# AI VS AI の対戦

まず、__下記__ のプログラムのように、__現在__ の __`play`__ メソッドで、__実引数__ に __`gui=True`__ を __記述__ して、__AI どうし__ の __対戦__ を行ってみることにします。__下記__ は、__`ai2`__ VS __`ai2`__ の __対戦__ です。

```python
%matplotlib widget
from marubatsu import Marubatsu
from ai import ai2

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

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/c07528c5-039d-e768-a254-df4e4a58a104.png" width="150">

__実行結果__ から、__ゲーム盤__ は __表示されます__ が、__AI の着手__ が __行われない__ ことがわかります。また、__ゲーム盤__ の __マスの上__ で __マウスを押す__ と、__着手__ を行うことが __できてしまう__ ことから、__AI どうし__ の __対戦__ を行ったにも __関わらず__、__人間どうし__ の __対戦__ が __行われてしまう__ という __問題が発生__ しています。このようなことがおきる理由ついて少し考えてみて下さい。

## 問題の検証

__`play`__ メソッドは、__下記__ のプログラムの __8 行目__ のように、__`gui`__ が __`True` の場合__ は、手番を人間と AI のどちらが担当していた場合でも、__ゲーム盤の画像__ を __描画した後__ で、__return 文__ を __実行__ して __`play`__ メソッドの __処理を終了__ します。また、__その後__ の __着手の処理__ は、__画像の上__ で __マウス__ が __押された__ 際に __実行__ される、__`on_mouse_down`__ によって __行われる__ ので、__人間__ が __着手を行う__ ことが __できてしまいます__。これが __上記の問題__ が発生する __原因__ です。

```python
 1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
中略
 2          # ゲームの決着がついていない間繰り返す
 3          while self.status == Marubatsu.PLAYING:
 4              # ゲーム盤の表示
 5              if verbose:
 6                  if gui:
 7                      self.draw_board(ax)
 8                      return
 9                  else:
10                      print(self)
11              # 現在の手番を表す ai のインデックスを計算する
12              index = 0 if self.turn == Marubatsu.CIRCLE else 1
13              # ai が着手を行うかどうかを判定する
14              if ai[index] is not None:
15                  x, y = ai[index](self, **params[index])
以下略                    
```

下記は、[前回の記事](https://qiita.com/ysgeso/items/c91d5373753253d98f23#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E9%A7%86%E5%8B%95%E5%9E%8B%E3%81%AE-play-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E3%83%95%E3%83%AD%E3%83%BC%E3%83%81%E3%83%A3%E3%83%BC%E3%83%88)で紹介した、__`gui`__ に __`True`__ が __代入__ されていた場合の、__`play`__ メソッドの __フローチャート__ です。図から、__`play`__ メソッドでは、__AI__ が __〇 の手番__ を __担当する場合__ でも、__図の中__ にある「__AI__ が __着手を選択する__」という __処理__ が、__実行されない__ ことが __わかります__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/216d2298-5210-24aa-00f2-646d19623007.png" width="350">

## 問題の修正

__AI どうし__ の __対戦__ の場合は、__すべての着手__ を __AI が行う__ ので、__`on_mousedown`__ による、__GUI__ の __入力の処理__ は __必要__ は __ありません__。従って、__人間が着手__ を __行う場合__ のように、__`play`__ メソッドを __終了する必要__ は __ありません__。

また、__AI__ が __着手を行う処理__ は、__上記__ のプログラムの __15 行目__ で行われます。従って、__AI__ が __着手を行う__ 場合は、__上記__ の __8 行目__ の __return 文__ を __実行しない__ ようにすれば、__15 行目__ が __実行__ されて __AI__ が __着手を行う__ ようになります。

__下記__ は、そのように __`play`__ メソッドを __修正__ したプログラムです。

* __8 行目__：この後の __13 行目__ で __`index`__ を __利用__ する __必要がある__ ので、__元__ は __17 行目の前__ にあった、__現在の手番__ を表す __`ai`__ の __インデックス__ を __計算__ する __処理__ を __ここに移動__ する
* __13 行目__：__現在の手番__ が __人間の場合__ に __return 文__ を __実行__ して __`play`__ メソッドを __終了__ する

```python
 1  import matplotlib.pyplot as plt
 2  import math
 3
 4  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
 5      # ゲームの決着がついていない間繰り返す
 6      while self.status == Marubatsu.PLAYING:
 7          # 現在の手番を表す ai のインデックスを計算する
 8          index = 0 if self.turn == Marubatsu.CIRCLE else 1
 9          # ゲーム盤の表示
10          if verbose:
11              if gui:
12                  self.draw_board(ax)
13                  if ai[index] is None:
14                      return
15              else:
16                  print(self)
17          # ai が着手を行うかどうかを判定する
18          if ai[index] is not None:
19              x, y = ai[index](self, **params[index])
元と同じなので省略
20    
21  Marubatsu.play = play
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
import matplotlib.pyplot as plt
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:
        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:
                self.draw_board(ax)
                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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
import matplotlib.pyplot as plt
import math

def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
+       index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                self.draw_board(ax)
-               return
+               if ai[index] is None:
+                   return
            else:
                print(self)
-       index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
元と同じなので省略
    
Marubatsu.play = play
```
</details>

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__実行結果__ のように、__AI どうし__ の __対戦__ が行われ、その __結果の画像__ が __表示__ されます。また、__下記のセル__ を __何度__ か __実行し直し__ て、__毎回異なる対戦結果__ が __表示__ されることを __確認__ してみて下さい。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/82ef1963-1182-12b7-cc44-ef5051f567a4.png" width="150">

## AI どうしの対戦で行われる処理のフローチャート

__下図左__ は、__`gui`__ が __`True`__ の場合に、__AI どうし__ の __対戦__ が行われる際の、__`play`__ メソッドの __フローチャート__ です。__下図右__ の、[前回の記事](https://qiita.com/ysgeso/items/c91d5373753253d98f23#%E3%83%95%E3%83%AD%E3%83%BC%E9%A7%86%E5%8B%95%E5%9E%8B%E3%81%AE-play-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E3%83%95%E3%83%AD%E3%83%BC%E3%83%81%E3%83%A3%E3%83%BC%E3%83%88)で紹介した、__`gui`__ が __`False`__ の場合に行われる __フロー駆動型__ の __`play`__ メソッドの __フローチャート__ と __見比べて__ 下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/76bc1aec-783b-07fd-2c0c-053e236a6c6a.png" width="300">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/677d2cef-66b0-5ff4-9466-d603b9f2ae16.png" width="300">

__Figure に関する処理__ などの __細かい部分__ で __いくつかの違い__ が __ありますが__、行われる __処理の流れ__ が __ほぼ同じ__ であることを __確認して下さい__。従って、__AI どうし__ の __対戦__ では、__フロー処理型__ の場合と __同様__ に、__決着がつくまで__ の __処理__ を __`play`__ メソッドで __行います__。

ところで、__上図左__ の __フローチャート__ から __わかる__ ように、__`gui`__ に __`True`__ が __代入__ されている場合は、__`play`__ メソッドは __イベントハンドラ__ を __登録__ するので、__`play`__ メソッドの __終了後__ も、__ゲーム盤の上__ で __マウスを押す__ と、__登録__ した __イベントハンドラ__ が __イベントループ__ から __呼び出されます__。そのため、__`play`__ メソッドを __実行後__ に __ゲーム盤の上__ で __マウスを押す__ と、__着手__ が __行われてしまう__ のではないかと __思う人__ が __いるかもしれません__。

__実際__ に、__`play`__ メソッドの __実行後__ に __ゲーム盤の上__ で __マウスを押す__ と __イベントハンドラ__ が __実行__ されますが、[前回の記事](https://qiita.com/ysgeso/items/c91d5373753253d98f23#%E6%B1%BA%E7%9D%80%E5%BE%8C%E3%81%AE%E7%9D%80%E6%89%8B%E3%81%AE%E7%A6%81%E6%AD%A2)で示した __下記__ の __フローチャート__ のように、__ゲーム__ の __決着後__ の __着手を禁止__ する __処理__ を __イベントハンドラ__ に __記述__ したので、__着手__ は __行われません__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/20326307-54d3-8bf3-41c4-c7f7809c7b1f.png" width="200">

## 無駄な描画の処理を行わないようにする

__下図__ は、__`gui`__ に __`True`__ が __代入__ されていた場合に、__`play`__ メソッドで __AI どうし__ の __対戦__ を __行った場合__ の __フローチャート__ の __一部__ です（__繰り返し__ の __前の部分__ を __省略__ しています）。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/803cd59e-f72a-7987-4b23-eaeb43a49f2d.png" width="300">

__図__ の __黄色い長方形__ が、__`draw_board`__ で __ゲーム盤の画像__ の __描画を更新__ する処理ですが、__図から__ わかるように、この処理は、__以下__ の __タイミング__ で __何度も実行__ されます。

* __繰り返し__ の処理の __中__ で、__決着がつくまで__ の __それぞれの局面__ を __描画__ する
* __繰り返し__ の処理の __後__ で、__決着がついた局面__ を __描画__ する

[以前の記事](https://qiita.com/ysgeso/items/975078999ad86bd38876#matplotlib-%E3%81%AE-figure-%E3%81%AE%E7%94%BB%E5%83%8F%E3%81%AE%E6%8F%8F%E7%94%BB%E3%81%8C%E8%A1%8C%E3%82%8F%E3%82%8C%E3%82%8B%E3%82%BF%E3%82%A4%E3%83%9F%E3%83%B3%E3%82%B0) で説明したように、__matplotlib__ の __画像__ の __描画の更新__ は、__JupyterLab__ の __セル__ のプログラムの __実行__ が __終了するまで__ は __行われない__ ので、__JupyterLab__ 上の __画像__ には、__AI どうし__ の __対戦__ の __途中経過__ は __描画されず__、__決着後の局面__ の __ゲーム盤のみ__ が __描画__ されます。

上記のことから、__AI どうし__ の __対戦__ の場合は、__対戦の途中経過__ を __描画__ するために呼び出した __`draw_board`__ メソッドの __処理__ は __無駄な処理__ であるということが __わかります__。__〇×ゲーム__ のような、__最大__ でも __9 手__ で __ゲームが終了__ するようなゲームの場合は、__決着__ が __ついていない間__ の __`draw_board`__ の __呼び出し__ は __最大__ でも __9 回__ しか行われないので、__`play`__ メソッドの __処理が遅い__ と __感じる__ ことは __ありません__ が、__ゲーム__ の __決着がつくまで__ にもっと __長い手数がかかる__ ようなゲームの場合は、__無駄__ な __`draw_board`__ の __呼び出し__ によって、__ゲーム__ の __決着がつくまで__ にかかる __時間__ が __長く感じられる__ ような __場合__ が __生じる可能性__ があります。

そこで、__下記__ のプログラムのように、__AI どうし__ の __対戦__ で、__ゲーム__ の __決着がついていない__ 場合は __`draw_board`__ メソッドを __呼び出さない__ ように __修正__ することにします。

* __10 行目__：「__AI どうし__ の __対戦__ の場合は __画像__ を __描画しない__」ということは、「__どちらか__ が __人間__ である場合に __画像を描画__ する」ということなので、__その条件式__ を __記述__ する

```python
 1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略          
 2      # ゲームの決着がついていない間繰り返す
 3      while self.status == Marubatsu.PLAYING:
 4          # 現在の手番を表す ai のインデックスを計算する
 5          index = 0 if self.turn == Marubatsu.CIRCLE else 1
 6          # ゲーム盤の表示
 7          if verbose:
 8              if gui:
 9                  # AI どうしの対戦の場合は画面を描画しない
10                  if ai[0] is None or ai[1] is None:
11                      self.draw_board(ax)
12                  # 手番を人間が担当する場合は、play メソッドを終了する
13                  if ai[index] is None:
14                      return
15              else:
16                  print(self)
元と同じなので省略          
17    
18  Marubatsu.play = play
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # 〇×ゲームを再起動する
    self.restart()

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)
        
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)                   

    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す 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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
元と同じなので省略          
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
-               self.draw_board(ax)
+               if ai[0] is None or ai[1] is None:
+                   self.draw_board(ax)
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
元と同じなので省略          
    
Marubatsu.play = play
```
</details>

:::note
__10 行目__ で、「__AI どうし__ の __対戦__ の場合は __画像__ を __描画しない__」という __条件式__ を __記述しない理由__ は、__その条件式__ は、__下記__ のような __複雑な条件式__ になるからです。

```python
if not((ai[0] is not None) and (ai[1] is not None)):
    self.draw_board(ax)
```
:::

実行結果は省略しますが、__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__AI どうし__ の __対戦__ が行われ、その __結果の画像__ が __表示__ されることが __確認__ できます。

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

__上記の修正__ は、__処理__ の __無駄を省く__ ための修正なので、__行わなくても__ プログラムは __正しく動作__ します。また、__この処理__ を __フローチャートに記述__ すると、フローチャートが __複雑__ になって __わかりづらくなる__ ので、以後の __`play`__ メソッドの __フローチャート__ を図で示す際に、__この処理__ は __省略__ することにします。

なお、__上記の修正__ を行うことで、__AI どうし__ の __対戦__ を行った際の __`play`__ メソッドの __処理速度__ は __実際__ に __短く__ なりますが、先程説明したように、__〇×ゲーム__ の場合は、__決着がつくまで__ の __手数が短い__ ので、__処理速度の差__ を __人間が体感__ することは __できません__。

[以前の記事](https://qiita.com/ysgeso/items/728cee8982fe0ed9da38#%E3%83%86%E3%82%B9%E3%83%88%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B)の __プログラムのテスト__ の記事で説明したように、__プログラムの修正__ を行った場合は、__新しい処理__ が __正しく動作する__ ことを __確認__ する __だけでなく__、__これまで__ に __動作していた処理__ が、引き続き __正しく動作するか__ どうかを __確認したほうが良い__ でしょう。そこで、__下記__ のプログラムで __`play`__ メソッドを __実行__ して、__人間 どうし__ の __対戦__ を行う場合も、__問題なく__ 対戦が __行える__ ことを __実際に確認__ してみて下さい。

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

# 人間 VS AI の対戦

__先程と同様__ に、__`play`__ メソッドの __実引数__ に __`gui=True`__ を __記述__ して、__下記__ のプログラムのように、__人間__ VS __AI__ の __対戦__ を __行ってみる__ ことにします。下記は、__人間__ VS __`ai2`__ の __対戦__ です。

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

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/c07528c5-039d-e768-a254-df4e4a58a104.png" width="150">

__実行結果__ のように、__ゲーム盤が表示__ されるので、__好きなマス__ の上で __マウスを押す__ とそのマスに __着手__ が __行われます__。__下記__ は、__(0, 0)__ のマスの上で __マウス__ を __押した場合__ の __図__ です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/f8f2554d-d714-205f-13e3-1f04cd32cebd.png" width="150">

__上図__ のように、__人間__ が __着手__ を __行った後__ で、__AI の着手__ は __行われません__。また、__上図__ でゲーム盤の __マスの上__ で __マウスを押す__ と、__着手__ を __行うこと__ が __できてしまう__ ため、__人間__ VS __AI__ の __対戦__ を __行おうとした__ のにも __関わらず__ 、__人間どうし__ の __対戦__ が __行われてしまう__ という __問題が発生__ しています。何故このようなことになるかについて少し考えてみて下さい。

## 問題の検証

__人間__ VS __AI__ の __対戦__ の場合は、__`gui`__ が __`True`__ なので、__`play`__ メソッドは、__下記__ のプログラムの __8 行目__ のように、__ゲーム盤の画像__ を __描画した後__ で、__return 文__ を __実行__ して __`play`__ メソッドの __処理を終了__ します。__ここまでの処理__ は __問題__ は __ありません__。

```python
 1  def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
中略
 2          # ゲームの決着がついていない間繰り返す
 3          while self.status == Marubatsu.PLAYING:
 4              # ゲーム盤の表示
 5              if verbose:
 6                  if gui:
 7                      self.draw_board(ax)
 8                      return
 9                  else:
10                      print(self)
以下略                    
```

__下図__ は、__`gui`__ が __`True`__ の場合に、__人間__ VS __AI__ の __対戦__ が行われる際の、__`play`__ メソッドの __フローチャート__ です。__赤線の処理__ が行われ、__`play`__ メソッドの __処理が終了__ します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e94272d8-d7d9-1ddd-102f-183ffd7cfedf.png" width="300">

__その後__ で、__ゲーム盤__ の __マスの上__ で __マウスを押す__ と、__下記__ の __`on_mouse_down`__ が __実行__ されて __着手を行う処理__ が __実行__ されます。

```python
        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)
```

__問題__ は、__上記__ の __`on_mouse_down`__ で __人間__ が __着手を行う処理__ を __行った後__ で、__AI__ が __着手を行う処理__ がどこにも __記述されていない__ 点です。また、__`on_mouse_down`__ の __処理__ を __行った後__ で、__ゲーム盤__ の __マスの上__ で __マウスを押す__ と、__再び `on_mouse_down`__ が __実行される__ ため、__AI の手番__ であるにも __関わらず__、__人間__ が __着手を行う__ ことが __できてしまいます__。

そのことは、__下記__ の __`on_mouse_down`__ の __フローチャート__ からも __確認__ できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0e803074-9126-8af5-87a1-030d6592a423.png" width="200">

__この問題__ は、__`on_mouse_down`__ が __着手の処理__ を __行った後__ で、「__次の手番__ を __AI が担当__ する場合は __その AI__ が __着手を選択__ する」という __処理を記述__ することで __解決__ することが __できます__。どのようにその処理を記述すれば良いかについて少し考えてみて下さい。

## 問題を修正したフローチャート

__下図__ は、__上記の処理を行__ うように __修正__ した __`on_mouse_down`__ の __フローチャート__ です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/75c875dc-c84e-c790-d6d8-b53a04b33cb1.png" width="200">

:::note
2024/04/14 修正：__フローチャート__ に __画像を描画する処理__ が __入っていませんでした__ が、入れたほうが良いと思いましたので、__入れるよう__ に __修正__ しました。
:::

__このように修正__ することで、__人間__ が __ゲーム盤__ の __画像の上__ で __マウス__ を __押すたび__ に __`on_mouse_down`__ が __呼び出され__、「__人間の着手を行う__」と「__AI の着手を行う__」の __処理__ が __続けて行われる__ ようになるため、__人間__ VS __AI__ の __対戦__ が __行われる__ ようになります。

## 問題の修正

__それぞれの手番__ を __人間__ と __AI__ の __どちらが担当するか__ を表す __情報__ は、__`play`__ メソッドの __仮引数 `ai`__ に __代入__ されています。__`on_mouse_down`__ は __`play`__ メソッドの __ローカル関数__ なので、その __ブロックの中__ で __`ai`__ を __そのまま利用__ することが __できます__。従って、__下記__ のプログラムのように __`on_mouse_down`__ を __修正__ することで、上記の __問題を解決__ できます。

* __13 行目__：マウスによる __着手__ が __行われた後__ の __手番__ の __`ai`__ の __インデックス__ を __計算__ する
* __14 行目__：__AI__ の __手番であるか__ どうかを __判定__ する
* __15 ~ 17 行目__：__AI の着手__ を __計算__ し、__着手を行い__、__ゲーム盤の画像__ を __更新__ する

```python
 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                  # 現在の手番を表す ai のインデックスを計算する
12                  index = 0 if self.turn == Marubatsu.CIRCLE else 1
13                  # ai が着手を行うかどうかを判定する
14                  if ai[index] is not None:                
15                      x, y = ai[index](self, **params[index])
16                      self.move(x, y) 
17                      self.draw_board(ax)
元と同じなので省略
18    
19  Marubatsu.play = play
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # 〇×ゲームを再起動する
    self.restart()

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)
        
                # 現在の手番を表す 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)
                    
        # 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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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 ai[index] is not None:                
+                   x, y = ai[index](self, **params[index])
+                   self.move(x, y) 
+                   self.draw_board(ax)
元と同じなので省略
    
Marubatsu.play = play
```
</details>

:::note
__マウス__ による __着手の選択__ と、__AI__ による __着手の選択__ の処理は、__下記の表__ のように __対応している__ ので __見比べて__ みて下さい。

| 処理 | 人間 | AI |
|:--|--:|--:|
| __着手を行う座標を計算する__  | 6、7 行目  | 15 行目   |
| __着手を行う__  | 8 行目  | 16 行目  |
| __画像を更新する__  | 9 行目  | 17 行目|
:::

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__人間が着手__ を行うと __自動的__ に __AI__ が __着手を行う__ ようになります。__下記__ の実行結果は、__(0, 0)__ の __マスの上__ で __マウスを押した__ 場合の図で、__AI__ が __(2, 0)__ のマスに __着手を行う__ ことが __確認__ できます。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b68a6c9c-a29a-6b16-132c-86478eb1634e.png" width="150">



__うまく__ プログラムが __動作している__ ように __見えるかもしれません__ が、実は、__上記__ のプログラムには __二つの問題__ があります。__実際__ に __AI__ と __何度か対戦__ を __行ってみて__、どのような __問題__ があるかを __探してみて__ 下さい。

## 二つの問題の検証

### 一つ目の問題

__一つ目の問題__ は、__〇 の着手__ によって __勝利した後__ で、__AI__ が __着手を行ってしまう__ というものです。__下記__ は、__〇 が勝利__ する __着手を行った場合__ の __図__ です。__〇 が勝利__ しているにも __関わらず__、__× を担当__ する __AI__ が __着手__ を __行っています__。__実際__ に __〇 が勝利__ する __着手を行った場合__ に __AI__ が __着手を行ってしまう__ ことを __確認__ してみて下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/519e0268-e1f0-f08d-04c9-8235a6539f83.png" width="150">

### 二つ目の問題

__二つ目の問題__ は、__9 手目__ の __着手__ を __行った場合__ に、__下記__ のような __エラーが発生__ してしまうというものです。__実際__ に __9 手目__ の __着手__ を __行った際__ に、__下記__ の __エラーが発生__ することを __確認__ してみて下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0d463cc6-eca7-0b3c-bbd7-497ebd650a30.png" width="150">

```
略
File c:\Users\ys\ai\marubatsu\071\ai.py:235, in ai2(mb)
    224 """ランダムなマスに着手する AI.
    225 
    226 Args:
   (...)
    231     着手する座標を表す tuple
    232 """
    234 legal_moves = mb.calc_legal_moves()
--> 235 return choice(legal_moves)

File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\random.py:373, in Random.choice(self, seq)
    370 # As an accommodation for NumPy, we don't use "if not seq"
    371 # because bool(numpy.array()) raises a ValueError.
    372 if not len(seq):
--> 373     raise IndexError('Cannot choose from an empty sequence')
    374 return seq[self._randbelow(len(seq))]

IndexError: Cannot choose from an empty sequence
```

:::note
__Python__ の __バージョン 3.10__ では、__下記__ のような __エラーメッセージ__ が __表示される__ ので、__バージョン__ によって、__同じエラー__ でも __表示__ される __エラーメッセージ__ が __若干変わる場合__ が __あるようです__。ただし、__エラーメッセージ__ が __若干が変わっても__、この後で説明する、__エラーメッセージ__ の __読み方__ は __変わりません__。

```
略
File c:\Users\ys\ai\marubatsu\071\ai.py:235, in ai2(mb)
    224 """ランダムなマスに着手する AI.
    225 
    226 Args:
   (...)
    231     着手する座標を表す tuple
    232 """
    234 legal_moves = mb.calc_legal_moves()
--> 235 return choice(legal_moves)

File c:\Users\ys\Anaconda3\envs\ml\lib\random.py:346, in Random.choice(self, seq)
    344 """Choose a random element from a non-empty sequence."""
    345 # raises IndexError if seq is empty
--> 346 return seq[self._randbelow(len(seq))]

IndexError: list index out of range
```
:::

## エラーメッセージの表記の意味

先に、__エラーメッセージ__ という __手がかり__ がある、__二つ目の問題__ から __検証__ することにします。__これまで__ は、__エラーメッセージ__ の中で、__最後__ に __表示された内容__ を見て __エラーの原因__ を __推測__ してきましたが、__上記__ の 「__空__（empty）の __シーケンス型__（sequence）から（from）__選択__（choose）することは __できない__（cannot）」という意味の __`
IndexError: Cannot choose from an empty sequence`__ というエラーメッセージ __だけから__、__エラーの原因__ を __推測__ することは __困難__ です。また、その上に表示される、エラーが発生した行を表す、__`--> 378 return seq[self._randbelow(len(seq))]`__ は、__自分で記述__ した __覚えのない__ プログラムであり、これを見ても __エラーの原因__ を __推測__ することは __困難__ です。

__このような場合__ は、__それより前__ の __エラーメッセージ__ を __さかのぼって__ 原因を __推測__ する __必要__ が __あります__。そのためには、__エラーメッセージ__ に __どのような情報__ が __表示__ されているかを __理解する必要__ が __あります__。

__関数__ や __メソッド__ は、__プログラム__ の __様々な場所__ から __呼び出され__ て __利用__ されます。そのため、__関数の中__ で __エラーが発生__ した場合は、__その関数__ が __どこから呼び出されたか__ の情報を __知ること__ が、__エラー__ が __発生するまで__ の __プログラム__ の __処理の流れ__ を __辿るため__ に __重要__ になります。そのため、__エラーメッセージ__ には、__エラー__ が __発生するまで__ に __呼び出された関数__ が __順番に記述__ されるようになっています。

### エラーメッセージの読み方の具体例

言葉の説明だけではわかりづらいので、__具体例__ を __挙げます__。__下記__ のプログラムは、__2 つ__ の __関数 `x`__ と __`y`__ を __定義__ し、__`y()`__ の __返り値__ を __表示__ するプログラムです。

```python
1  def x():
2      return a
3
4  def y():
5      return x()
6
7  print(y())
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def x():
    return a

def y():
    return x()

print(y())
```
</details>

__上記__ の __プログラム__ の __処理の流れ__ は以下のようになります。

1. __7 行目__ で __`y()`__ を __実行__ して __関数 `y`__ を __呼び出す__
2. __4 行目__ の __関数 `y`__ が __呼び出される__
3. __5 行目__ で __`x()`__ を __実行__ して __関数 `x`__ を __呼び出す__
4. __1 行目__ の __関数 `x`__ が __呼び出される__
5. __2 行目__ で __`a`__ を __関数 `x`__ の __返り値__ として __返す__
6. __5 行目__ で __関数 `x`__ の __返り値__ を、__関数 `y`__ の __返り値__ として __返す__
7. __7 行目__ で __関数 `ｙ`__ の __返り値__ を __`print`__ で __表示__ する

ただし、__実際__ には、__2 行目__ の __`a`__ は __関数 `x`__ の __ブロックの中__ で __一度__ も __値が代入されていない__ ので、__2 行目__ を __実行__ すると、__下記__ の __実行結果__ の __最後の行__ のように、__`a`__ が __定義されていない__ という __意味__ の __`NameError: name 'a' is not defined`__ という __エラーが発生__ します。

実行結果

```
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[11], line 7
      4 def y():
      5     return x()
----> 7 print(y())

Cell In[11], line 5
      4 def y():
----> 5     return x()

Cell In[11], line 2
      1 def x():
----> 2     return a

NameError: name 'a' is not defined
```

__上記__ の __エラーメッセージ__ の中の、__最初の行__ に __表示__ される __`Traceback (most recent call last)`__ は、__最も最近__（most recent）に __呼び出された__（call）ものを __最後__（last）に __表示__ するという __順番__ で、プログラムの __処理__ を __さかのぼって__（Traceback）__表示__ するという __意味__ です。実際に、__エラーメッセージ__ では、__エラー__ が __発生するまで__ に __実行__ された __処理の流れ__ が、__関数__ が __呼び出された順__ で __表示__ されています。

__エラーメッセージ__ の表示の中の、__`Cell In [11], line 7`__ のような __表示__ と、__`---->`__ が __表示された行__ は、__以下__ のような __意味__ を持ちます。

* __エラー__ が __発生するまで__ に __実行__ した __関数呼び出し__ が __記述__ されている __行の情報を__ 表す。ただし、__最後に表示__ されたものは、__エラー__ が __発生した行__ の __情報__ を表す
* __`Cell In [11], line 7`__ のような __表記__ は、__関数呼び出し__ が行われた（または __エラーが発生__ した）__JupyterLab__ の __セルの番号__ と、__そのセルの中__ の __行数__ を表す
* __`---->`__ が __記述__ されている __行__ が、__関数呼び出し__ が行われた（または __エラーが発生__ した）__行の内容__ を表す

__上記__ の __エラーメッセージの右__ に、上記の __処理の手順__ の __対応を記述__ したものを __下記__ に __記します__。__エラーメッセージ__ が、__処理の手順__ の __順番通り__ に __エラーが発生するまで__ の __処理の流れ__ を __表記__ していることを __確認__ して下さい。
```
Cell In[11], line 7
      4 def y():
      5     return x()
----> 7 print(y())                   1. y() を実行して 関数 y を呼び出す

Cell In[11], line 5
      4 def y():                     2. 関数 y が呼び出される
----> 5     return x()               3. x() を実行して 関数 x を呼び出す

Cell In[11], line 2
      1 def x():                     4. 関数 x が呼び出される
----> 2     return a                 5. a を 関数 x の返り値として返す

NameError: name 'a' is not defined   上記の手順 5 でこのエラーが発生した
```

__エラーメッセージ__ を __読み解く際__ には、__下から見る__ のが __一般的__ です。__その理由__ は、__下から見たほう__ が __エラーの原因__ を __見つけやすい場合__ が __多い__ からです。例えば、__上記__ の __エラーメッセージ__ の場合は、__`NameError: name 'a' is not defined`__ と __その上部__ に記述された __`x` の定義__ の部分を __見るだけ__ で、__`a`__ が __定義されていない__ ことが __原因__ であることが __わかります__。

### 二つ目のエラーメッセージの検証

__下記__ は、先程の __二つ目__ の __エラーメッセージ__ を __再掲__ したものです。__上記の例__ は、__JupyterLab__ の __セルに記述__ された __プログラム__ でしたが、__`play`__ メソッドの __中__ では、__インポート__ した __モジュール__ の __ファイル__ に __記述__ された __プログラムが実行__ される場合があります。__そのような場合__ は、`Cell` ではなく、__`File` で始まる行__ で、__下記__ のような __方法__ で __関数呼び出し__（または __エラー__）の __情報__ が __記述__ されます。

* `File ファイルのパス:行数, in その下のプログラムが記述された関数名(仮引数)`

```
略
File c:\Users\ys\ai\marubatsu\071\ai.py:235, in ai2(mb)
    224 """ランダムなマスに着手する AI.
    225 
    226 Args:
   (...)
    231     着手する座標を表す tuple
    232 """
    234 legal_moves = mb.calc_legal_moves()
--> 235 return choice(legal_moves)

File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\random.py:373, in Random.choice(self, seq)
    370 # As an accommodation for NumPy, we don't use "if not seq"
    371 # because bool(numpy.array()) raises a ValueError.
    372 if not len(seq):
--> 373     raise IndexError('Cannot choose from an empty sequence')
    374 return seq[self._randbelow(len(seq))]

IndexError: Cannot choose from an empty sequence
```

この __エラーメッセージ__ を __下から見る__ と、__下記__ のようなことが __わかります__。

* __エラー__ が __発生した行__ の __内容__ は、__`raise IndexError('Cannot choose from an empty sequence')`__ である
* __`File:(略)\random.py:373`__ から、__エラー__ が __発生した行__ は、__random.py__ という __ファイル__ の __373 行目__ に __記述__ されている
* __`in Random.choice(self, seq)`__ から、__373 行目__ は、__`Random`__ という __クラス__ の __`choice`__ という __メソッドで定義__ されており、この __メソッド__ には、__`seq`__ という __仮引数__ が __存在__ する

:::note
__373 行目__ の __`raise`__ は、その __後ろに記述__ した __エラー__ を __発生する処理__ を行います。__実際__ に __`raise`__ の __後ろ__ に __記述__ されている __内容__ が、__最後の行__ の __エラーメッセージ__ と __対応__ していることを __確認__ して下さい。

__`raise`__ の使い方に関しては、必要になった時点で紹介する予定です。
:::

__上記__ から __random__ モジュールの __`choice`__ メソッドで __エラーが発生__ したことが __わかります__。

また、__`Cannot choose from an empty sequence`__ という __エラーメッセージ__ から、おそらく __`choice`__ の __実引数__ に __空のシーケンス型__ の一つである __空の list__ を __記述__ して __呼び出した__ ことが __原因__ で __エラーが発生__ したことが __推測されます__。

__そのこと__ を __確認__ するために、__下記__ のプログラムのように、__random__ モジュールの __`choice`__ の __実引数__ に、__空の list__ を __記述__ して __実行__ すると、確かに __同じエラー__ が __発生__ します。

```python
from random import choice

print(choice([]))
```

実行結果

```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[12], line 3
      1 from random import choice
----> 3 print(choice([]))

File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\random.py:373, in Random.choice(self, seq)
    370 # As an accommodation for NumPy, we don't use "if not seq"
    371 # because bool(numpy.array()) raises a ValueError.
    372 if not len(seq):
--> 373     raise IndexError('Cannot choose from an empty sequence')
    374 return seq[self._randbelow(len(seq))]

IndexError: Cannot choose from an empty sequence
```

__`choice`__ の __実引数__ に __空の list__ を __記述__ して __実行__ したことが __原因__ であることが __わかりました__ が、何故 __そのような処理__ が __行われたか__ についてまでは __わかりません__。そこで、__下記__ の、__先程__ の __エラーメッセージ__ の __一つ上に表示__ された __メッセージ__ を __検証__ することにします。

```
略
File c:\Users\ys\ai\marubatsu\071\ai.py:235, in ai2(mb)
    224 """ランダムなマスに着手する AI.
    225 
    226 Args:
   (...)
    231     着手する座標を表す tuple
    232 """
    234 legal_moves = mb.calc_legal_moves()
--> 235 return choice(legal_moves)
```

__上記__ の __エラーメッセージ__ から、__下記__ のようなことが __わかります__。

* __確かに__、__`return choice(legal_moves)`__ によって、__`choice`__ に __実引数  `legal_moves`__ を __記述__ して __呼び出している__
* __`File:(略)\ai.py:235`__ から、__`choice`__ を __呼び出した行__ は、__`ai.py`__ という __ファイル__ の __235 行目__ に __記述__ されている
* __`in ai2(mb)`__ から、__この行__ は、__`ai2`__ という __関数で定義__ されており、__この関数__ には、__`mb`__ という __仮引数__ が __存在__ する

__上記__ から __`choice`__ は、__`ai.py`__ に __記述__ された __`ai2`__ から __呼び出された__ ことが __わかります__。

__`ai.py`__ は、__これまでに定義__ した __AI__ を __記述__ した __モジュール__ で、__`ai2`__ は、先程 __`play`__ メソッドを __実行した際__ に、__× の手番__ を __担当__ するように __指定__ しました。

__`ai2`__ は、__自分で定義__ した __関数__ なので、その中で __行う処理__ は __わかるはず__ です。__忘れた方__ は、[以前の記事](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#ai2-%E3%81%AE%E5%AE%9A%E7%BE%A9)を __復習__ してください。__下記__ は __`ai2` の定義__ です。__`ai2`__ は、__ランダムな着手__ を行う __AI__ で、__`ai2`__ が __行う処理__ は、__合法手__ の __中から__、__random モジュール__ の __`choice`__ を使って __ランダム__ に __選択を行う__ というものです[^1]。


```python
def ai2(mb):
    legal_moves = mb.calc_legal_moves()
    return choice(legal_moves)
```

今回の __エラーが発生__ した __状況__ は、__9 手目__ の __着手__ を __行った場合__ です。__上記__ の __エラーメッセージ__ から、__`ai2`__ が __呼び出されている__ ことがわかるので、__9 手目__ の __着手__ を __行った後__ で、__`ai2`__ が呼び出されて __着手を選択__ する __処理を行った__ ことがわかります。

__〇×ゲーム__ の __ゲーム盤__ の __マスの数__ は __9 マス__ なので、__9 手目__ の __着手__ を __行った後__ は、__すべてのマス__ が __埋まっている__ ため、__合法手__ は __存在しません__。従って、__`ai2`__ の __2 行目__ の __`legal_moves = mb.calc_legal_moves()`__ を __実行__ すると、__`legal_moves`__ には __空の list__ が __代入__ され、__3 行目__ の __`return choice(legal_moves)`__ は、__空の list__ を __実引数に記述__ して __`choice`__ を __呼び出す__ ことになります。これで、__エラー__ が __発生した原因__ が __判明__ しました。

このようなことがおきるのは、__9 手目__ の __着手後__ に、__AI__ が __着手を行ってしまう__ ことが __原因__ です。従って、__9 手目__ の __着手後__ に、__AI__ が __着手__ を __行わないようにする__ ことで、この __問題__ を __解決できる__ ことが __わかりました__。

:::note warn
上記のように、__エラーメッセージ__ を __読み解く__ ことで、__エラーの原因__ が __判明する場合__ が __あります__。もちろん、__エラーメッセージ__ を __読み解くだけ__ で __すべて__ の __エラーの原因__ が __わかる__ とは __限りません__ が、__エラーメッセージ__ には、__エラーの原因__ の __重要なヒント__ が __記述__ されている __場合が多い__ ので、__エラーメッセージ__ の __意味を理解__ することは __デバッグ__ の __能力の向上__ に __つながる__ ため __非常に重要__ です。
:::

:::note
__先程__ の __エラーメッセージ__ の __最初__ には、__下記__ のような __メッセージが表示__ されます。__ファイルのパス__ の中に __`ipympl`__ が __記述__ されていることと、__メッセージ__（イベントのこと）を __処理__ する（handle）という __意味__ の __`_handle_message`__ という __名前__ の __関数__ で __処理が行われている__ ことから、__イベントループ__ から、__ipympl__ の仕組みを使ってで登録した __イベントハンドラ__ の __処理__ が __呼び出された__ ことが __推測__ できます。

```
File c:\Users\ys\Anaconda3\envs\marubatsu\Lib\site-packages\ipympl\backend_nbagg.py:279,
in Canvas._handle_message(self, object, content, buffers)
```
:::

## 一つ目と二つ目の問題の整理と統合

下記は、__上記の検証__ で __判明__ した __一つ目__ と __二つ目__ の __問題点__ です。

|  | 原因 |
|:--|:--|
|  __一つ目の問題__ | __〇 の勝利後__ に __AI が着手__ する |
|  __二つ目の問題__ | __9 手目の着手後__ に __AI が着手__ する  |

__上記__ から、__いずれの場合__ も、__特定の状況__ の __後__ で、__AI__ が __着手してしまう__ ことが __問題__ であることが __わかります__。従って、この __2 つ__ の __それぞれの状況__ に対して、__AI__ が __着手を行わないようにする__ ことで、__問題__ を __解決__ することが __できます__ が、__実は__ 上記の __二つの状況__ を、__一つの状況__ に __まとめる__ ことで、__より簡単__ に __問題__ を __解決する__ ことが __できます__ 。それが何かについて少し考えてみて下さい。

__上記__ の __2 つの問題__ の __状況__ に __共通する__ のは、__ゲーム__ が __決着した状況__ であるということです。また、__深く考えなくても__ 下記のことはすぐにわかると思います。

* __ゲーム__ が __決着していない__ 状況では、__AI__ は __着手__ を __行う必要__ がある
* __ゲーム__ が __決着した__ 状況では、__AI__ は __着手__ を __行ってはいけない__

従って、__ゲーム__ が __決着した__ 状況で、__AI__ が __着手__ を __行わないようにする__ ことで、__二つの問題__ を __同時に解決__ することが __できる__ ことが __わかります__。

### 問題の解決とフローチャート

__問題の原因__ は、__`on_mouse_down`__ が __行う処理__ が、__人間__ が __着手__ を __行った後__ で、__ゲーム__ の __決着がついているか__ どうかに __関係なく__、__AI__ が __着手を選択__ する __点__ なので、__下記__ のプログラムの __14 行目__ のように、__ゲーム__ の __決着がついていない場合のみ__、__AI__ が __着手を行うよう__ に __修正__ することで、__問題__ を __解決__ することが __できます__。

```python
 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                  # 現在の手番を表す ai のインデックスを計算する
12                  index = 0 if self.turn == Marubatsu.CIRCLE else 1
13                  # ゲームの決着がついていない場合に、ai が着手を行うかどうかを判定する
14                  if self.status == Marubatsu.PLAYING and ai[index] is not None:         
15                      x, y = ai[index](self, **params[index])
16                      self.move(x, y) 
17                      self.draw_board(ax)
元と同じなので省略                  
18    
19  Marubatsu.play = play
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # 〇×ゲームを再起動する
    self.restart()

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        fig, ax = plt.subplots(figsize=[size, size])
        fig.canvas.toolbar_visible = False
        fig.canvas.header_visible = False
        fig.canvas.footer_visible = False
        fig.canvas.resizable = False          
        
        # ローカル関数としてイベントハンドラを定義する
        def on_mouse_down(event):
            # Axes の上でマウスを押していた場合のみ処理を行う
            if event.inaxes and self.status == Marubatsu.PLAYING:
                x = math.floor(event.xdata)
                y = math.floor(event.ydata)
                self.move(x, y)                
                self.draw_board(ax)
        
                # 現在の手番を表す 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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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 ai[index] is not None:             
+               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
```
</details>

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7a354529-b600-7e95-b9a9-e48f4dec7d90.png" width="200">

__上記__ の __修正後__ に、__下記__ のプログラムで __`play`__ メソッドを __実行__ すると、__下記__ の __2 つ__ の __実行結果__ のように、__人間の着手__ で __ゲーム__ の __決着がついた__ 場合に、__AI__ が __着手__ を __行わなくなる__ ことが __確認__ できます。

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

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

__下図の後__ で、__×__ は __着手しません__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/2bd38f5e-a213-ac01-0e29-c35662966e1c.png" width="150">

__下図の後__ で、__エラー__ は __発生ません__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0d463cc6-eca7-0b3c-bbd7-497ebd650a30.png" width="150">

# AI VS 人間 の対戦

__先程と同様__ に、__`play`__ メソッドで __実引数__ に、__`gui=True`__ を __記述__ して __下記__ のプログラムのように、__AI__ VS __人間__ の __対戦__ を __行う__ ことにします。__下記__ は、__`ai2`__ VS __人間__ の __対戦__ です。

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

__実行結果__ は __示しません__ が、問題なく __AI__ VS __人間__ の __対戦が行える__ ことが __確認__ できます。__実際__ に __対戦を行って__、__問題がない__ ことを __確認__ してみて下さい。

## AI VS 人間の対戦で行われる処理

__対戦__ を __問題なく行う__ ことが __できる理由__ は、__`play`__ メソッドを __実行__ した際に、__下記__ のような __処理__ が __行われる__ からです。

* __〇__ を __AI が担当__ するので、__下記__ の __`play`__ メソッドの __9 行目__ で __`ai[index]`__ は __`None` ではない__ ため、__10 行目__ の __return 文__ は __実行されない__
* そのため、__15 行目__ が __実行され__ て __AI__ が __着手を選択__ する
* __次の繰り返し__ 処理では、__×__ を __人間が担当__ するので、__下記__ の __`play`__ メソッドの __9 行目__ で __`ai[index]`__ は __`None` になる__ ため、__10 行目__ の __return 文__ が __実行__ されて __`play`__ メソッドの __処理が終了__ する
* __その後__ は、__ゲーム盤の上__ で __マウスを押す__ ことで「__人間の着手__」と「__AI の着手__」の __処理__ が、__人間__ VS __AI__ と __同様の手順__ で __行われる__

```python
略
 1  # ゲームの決着がついていない間繰り返す
 2  while self.status == Marubatsu.PLAYING:
 3      # 現在の手番を表す ai のインデックスを計算する
 4      index = 0 if self.turn == Marubatsu.CIRCLE else 1
 5      # ゲーム盤の表示
 6      if verbose:
 7          if gui:
 8              self.draw_board(ax)
 9              if ai[index] is None:
10                   return
11          else:
12              print(self)
13      # ai が着手を行うかどうかを判定する
14      if ai[index] is not None:
15          x, y = ai[index](self, **params[index])
略
```        
## AI VS 人間の対戦のフローチャート

__下図__ は、__`gui`__ に __`True`__ が __代入__ されていた場合に、__`play`__ メソッドで __AI__ VS __人間__ の __対戦__ を __行った場合__ の __フローチャート__ です。__赤い線__ が、__1 回目__ の __繰り返し__ で行われる __処理__ を、__緑の線__ が、__2 回目__ の __繰り返し__ で行われる __処理__ を表します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/6d347226-ae1c-4df4-1eac-b7274c301174.png" width="350">

図から、__1 手目__ の __〇 の手番__ は __AI__ が __着手を行い__、__2 手目__ の __着手__ を __行う前__ に __`play`__ メソッドが __終了__ することが __わかります__。__以後__ は、__人間__ VS __AI__ の場合と __同様__ に、__ゲーム盤の上__ で __マウスを押す__ と、__`on_mouse_down`__ が __呼び出され__、__人間の着手__ と、__AI の着手__ が __続けて行われます__。そのため、__上記__ のプログラムで __AI__ VS __人間__ の __対戦__ を行うことが __できます__。

これで、__GUI__ による __AI との対戦__ の __実装が完了__ しました。最後に __念のため__、__上記の修正__ を行っても __人間__ VS __人間__ と __AI__ VS __AI__ の __対戦__ が __問題なく行えること__ を __確認__ してみて下さい。

# 今回の記事のまとめ

今回の記事では、__GUI__ で __AI との対戦__ が __行える__ ようにしました。

また、その際に、__エラーメッセージ__ の __読み方__ について __説明__ しました。__エラーメッセージ__ は __デバッグ__ を __行う際__ に __重要な手掛かり__ となるので、__正しい読み方__ を __理解する__ ことを __強くお勧めします__。

__次回の記事__ では、__GUI__ で __ゲームのリセット__ などを __行える__ ようにします。

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

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

https://github.com/ysgeso/marubatsu/blob/master/071/marubatsu.ipynb

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

https://github.com/ysgeso/marubatsu/blob/master/071/marubatsu_new.py

# 次回の記事

https://qiita.com/ysgeso/items/e7b226cb567e08ca2767

| 更新日時 | 更新内容 |
|:--|:--|
| 2024/04/14 | フローチャートに画像を描画する処理を表記するように修正しました |

[^1]:__[`ai2s`](https://qiita.com/ysgeso/items/8f3d4b3d2c02ef51a70e#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%A2%E6%95%B0%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E3%83%AB%E3%83%BC%E3%83%AB-2-%E3%81%A7%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%86-ai-%E3%81%AE%E5%AE%9A%E7%BE%A9)__ も __同じ処理__ を行いますが、__`ai2`__ のほうが __わかりやすい__ ので、今回の記事では __`ai2` を採用__ しました