Pythonで〇×ゲームのAIを一から作成する　その39　単純なAIの作成

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/1800a353cb609a0fef4e

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

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

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

## 前回までのおさらい

前回の記事で、〇×ゲームを遊ぶための `play` メソッドの実装が完了しました。

今回の記事からいよいよ 〇×ゲームの AI の作成を開始します。

# 〇×ゲームの AI の処理を行う関数

〇×ゲームの __AI が行う処理__ は、〇×ゲームの __局面__ から、次に __着手__ を行う __マスの座標を計算__ するというものです。これから、さまざまな〇×ゲームのAI を作成することになるので、作成する __AI ごと__ に、下記のような処理を行う __関数を定義__ することにします。

__処理__：〇×ゲームの __局面__ から、次に __着手__ を行う __マスの座標__ を __計算__ する
__名前__：それぞれの __AI ごと__ に `ai1`、`ai2` のような __名前をつける__
__入力__：__`mb`__ という名前の __仮引数__ に __`Marubatsu`__ クラスの __インスタンスを代入__ する
__出力__：__返り値__ として、次に __着手__ を行う __マスの座標__ を表す  __`(x, y)`__ という __tuple__ を __返す__

さまざまな AI を作成するので、それぞれの AI の関数の名前を凝った名前にすると、__名前を考える__ のが __大変__ になります。そこで、__`ai1`__ のような __番号を使った名前__ にします。

具体的には下記のような関数を定義します。

```python
def ai1(mb):
    mb から着手を行う座標 (x, y) を計算する処理
    return x, y
```

本記事では、〇×ゲームの __AI の関数__ は、`Marubatsu` クラスの __メソッドではなく__、__関数として定義__ することにします。その理由の一つは、`Marubatsu` クラスのメソッドとして AI の処理を記述すると、__AI の数だけ__ メソッドを __定義する必要__ が生じるため、__`Marubatsu`__ クラスの __定義__ が __非常に長く__ なってしまい、プログラムが __わかりづらく__ なるためです。他の理由としては、〇×ゲームを __管理して遊ぶための処理__ と、__AI の処理__ は __別の処理__ なので、別々に記述したほうがよいだろうと判断したためです。なお、`Marubatsu` クラスの __メソッドとして__ AI を __定義__ することは実際に __可能__ で、そのような記述が __間違っているわけではありません__ ので、そちらの方が良いと判断した場合は、そのように記述してもかまいません。

# 最初に見つかった空いているマスに着手を行う AI

最初に作成する AI として、__最も簡単__ に __作成__ できる、下記のような __アルゴリズム__ で着手を行う `ai1` という名前の関数を定義することにします。

1. 左上のマスから順番に __空いているマス__ を __探す__
2. __最初に見つかった__ 空いている __マス__ を着手を行うマスとして __選択__ する

:::note
上記の手順 1 では、左上のマスから順番にとしていますが、空いているマスを __探す順番__ を、例えば右下から順番に探すように __変更__ しても __構いません__。
:::

上記のようなアルゴリズムで着手を行う AI は、全然 AI のように見えないかもしれませんが、これでも __れっきとした AI__ です。もちろん、このアルゴリズムで作成された AI は強い AI には決してなりませんが、__AI と人間の対戦__ や、__AI どうしの対戦__ を行うことができます。

## `ai1` の定義

下記は上記のアルゴリズムで着手を選択するプログラムです。

* __2、3 行目__：__2 重の for 文__ によって、`mb` の __すべてのマス__ に対する __繰り返し処理__ を行う
* __4、5 行目__：__(x, y)__ のマスが __空であるか__ どうかを __判定__ し、__空の場合__ は __(x, y) を返す__

```python
from marubatsu import Marubatsu

def ai1(mb):
    for y in range(mb.BOARD_SIZE):
        for x in range(mb.BOARD_SIZE):
            if mb.board[x][y] == Marubatsu.EMPTY:
                return x, y
```

:::note
上記の __`ai1`__ は、__空いているマス__ が一つも __存在しない場合__ は、__5 行目__ の __return 文__ が __実行されることは無い__ ので、__2 重の for 文__ が __必ず終了__ し、__return 文__ が __実行されず__ に __関数__ のブロックの __処理が終了__ するので、__`None`__ が返り値として __返ります__。

ただし、__〇×ゲーム__ では、__空いている__ マスが一つも __存在しない場合__ は、__必ず__ ゲームの __決着がついている__ ので、その場合に __`ai1`__ が __呼ばれること__ は __あり得ない__ ので、その点について __気にする必要はない__ でしょう。
:::

下記は、ゲームの __開始直後__ と、__(0, 0) に着手を行った後__ で __`ai1`__ を呼び出して __次の着手を表示__ するプログラムです。実行結果から、ゲームの __開始直後__ の 4 行目の場合は、__左上__ の __(0, 0)__ のマスが __空いている__ ので __(0, 0)__ が、__(0, 0)__ に __着手を行った後__ の 6 行目の場合は、__その隣__ の __(1, 0)__ が __表示される__ ことが確認できます。

```python
mb = Marubatsu()
print(ai1(mb))
mb.move(0, 0)
print(ai1(mb))
```

実行結果
```
(0, 0)
(1, 0)
```

## `ai1` どうしの対戦

下記は __`ai1` どうし__ で __対戦__ を行うプログラムです。このプログラムは、__`play`__ メソッドの __ブロックの中__ で行っている処理を __抜き出して__、`input` を使って __キーボードから着手を行う座標を入力__ する __部分__ を、__`ai1` を呼び出して AI が着手を選択する__ ように __修正__ しています。

なお、__`ai1`__ は、__必ず__ ゲーム盤の __マスを表す座標__ を __返す__ ので、__`play`__ メソッドにあった、__`"exit"` に関する処理など__ の、__行う必要がない処理__ は __削除__ しました。実行結果から、__左上のマスから順番__ に __着手__ が行われ、7 手目で 〇 が勝利することが確認できます。

```python
mb = Marubatsu()
while mb.status == Marubatsu.PLAYING:
    print(mb)
    x, y = ai1(mb)
    mb.move(x, y)
print(mb)
```

実行結果
```
Turn o
...
...
...

Turn x
O..
...
...

Turn o
oX.
...
...

Turn x
oxO
...
...

Turn o
oxo
X..
...

Turn x
oxo
xO.
...

Turn o
oxo
xoX
...

winner o
oxo
xox
O..
```

## `play` メソッドの改良

__`ai1` と人間__ の __対戦__ も、__`play`__ メソッドの __処理を抜き出して修正__ することで行うことができますが、毎回そのようなプログラムを記述するのは __大変__ です。また、__行う処理__ は __`play`__ メソッドで行う処理と __大きく変わらない__ ので、__`play`__ メソッドを、「__人間どうし__」、「__人間と AI__」、「__AI どうし__」の __対戦__ を __行うことができる__ ように __改良__ することにします。どのように修正すればよいかについて少し考えてみて下さい。

### 〇 の担当の選択の実装

まず、__〇__ を __人間が担当__ するか、__`ai1` が担当__ するかを __選択できるよう__ に __改良__ することにします。そのためには、__`play`__ メソッドに、そのことを __選択できるようにする__ ための __仮引数を追加__ する必要があります。そこで、__`ai_circle`__ という名前の __仮引数__ に、__`True` が代入__ された場合は __`ai1`__ が、__`False` が代入__ された場合は __人間__ が __〇 を担当__ することにします。下記は、そのように `play` メソッドを修正したプログラムです。

* __9、10 行__：__〇 の手番__ で、なおかつ __`ai_circle` が `True`__ の場合に 10 行目で __`ai1` を呼び出し__、その __返り値__ を展開して __着手__ を行う __座標__ を __`x` と `y` に代入__ する
* __11 ~ 26 行目__：9 行目の if 文の __条件式が `False`__ の場合は、__元の処理__ を行う

```python
 1  def play(self, ai_circle):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.status == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          print(self)
 8          # 〇の手番で、ai_circle が True の場合は ai が着手を行う
 9          if self.turn == Marubatsu.CIRCLE and ai_circle:
10              x, y = ai1(self)
11          else:
12              # キーボードからの座標の入力
13              coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
14              # "exit" が入力されていればメッセージを表示して関数を終了する
15              if coord == "exit":
16                  print("ゲームを終了します")
17                  return       
18              # x 座標と y 座標を要素として持つ list を計算する
19              xylist = coord.split(",")
20              # xylist の要素の数が 2 ではない場合
21              if len(xylist) != 2:
22                  # エラーメッセージを表示する
23                  print("x, y の形式ではありません")
24                  # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
25                  continue
26              x, y = xylist
27          # (x, y) に着手を行う
28          try:
29              self.move(int(x), int(y))
30          except:
31              print("整数の座標を入力して下さい")
32
33      # 決着がついたので、ゲーム盤を表示する
34      print(self)
35
36  Marubatsu.play = play
```

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

```python
def play(self, ai_circle):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # 〇の手番で、ai_circle が True の場合は ai が着手を行う
        if self.turn == Marubatsu.CIRCLE and ai_circle:
            x, y = ai1(self)
        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("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

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

分かりづらくなるので、__else のブロックのインデント__ は __修正箇所__ に __含めません__。

```diff_python
-def play(self):
+def play(self, ai_circle):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # 〇の手番で、ai_circle が True の場合は ai が着手を行う
+       if self.turn == Marubatsu.CIRCLE and ai_circle:
+           x, y = ai1(self)
+       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("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>


下記のプログラムを実行すると、__〇 を `ai1`__ が、__× を人間__ が __担当__ して 〇×ゲームを遊ぶことができます。下記の実行結果は 1,1、2,2 の順でテキストボックスに入力した場合で、__`ai1`__ は、__アルゴリズムの通り__ に (0, 0)、(1, 0)、(2, 0) の順で着手を行い、〇 の勝利となります。

```python
mb.play(True)
```
実行結果
```
Turn o
...
...
...

Turn x
O..
...
...

Turn o
o..
.X.
...

Turn x
oO.
.x.
...

Turn o
oo.
.x.
..X

winner o
ooO
.x.
..x
```

__新しい機能を実装__ した __場合__ は、新しい機能を __実装した結果__、__従来の機能__ が __正しく動作しなくなる可能性__ があります。そのため、__新しい機能__ が __正しく動作するか__ どうかの __確認だけでなく__、__従来の機能__ が __正しく動作するかどうか__ の __確認__ を行う __必要__ があります。

下記のプログラムを実行すると、これまでと同様に、__人間どうし__ で〇×ゲームを遊ぶことができます。下記の実行結果は 0,0、0,1、1,0、1,1、2,0 の順でテキストボックスに入力した場合で、人間どうしの対戦を問題なく行えることが確認できます。

```python
mb.play(False)
```

実行結果
```
Turn o
...
...
...

Turn x
O..
...
...

Turn o
o..
X..
...

Turn x
oO.
x..
...

Turn o
oo.
xX.
...

winner o
ooO
xx.
...

```

### × の担当の選択の実装

次に、__×__  を __人間が担当__ するか、__`ai1` が担当__ するかを __選択できる__ ように __修正__ します。__先ほどと同様__ に、__`play`__ メソッドに、そのことを選択できるようにするための __`ai_cross`__ という名前の __仮引数を追加__ し、__処理を記述__ ます。下記は、そのように修正したプログラムです。

* __9、10 行目__：元の 条件式の条件または、__× の手番__ で、なおかつ __`ai_cross` が `True`__ の場合を判定するように条件式を修正する

なお、`play` メソッドがかなり長くなったので、以後は __長い__ 関数やメソッドの __定義__ と __修正箇所__ は、__修正__ した部分の __付近__ のプログラム __のみを表記__ することにします。ただし、__行番号のないプログラム__ のほうには __すべてのプログラムを記述__ します。

```python
 1  def play(self, ai_circle, ai_cross):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.status == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          print(self)
 8          # ai が着手を行うかどうかを判定する
 9          if (self.turn == Marubatsu.CIRCLE and ai_circle) or \
10             (self.turn == Marubatsu.CROSS and ai_cross):
11              x, y = ai1(self)
12          else:
13              # キーボードからの座標の入力
以下同じなので省略
```

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

```python
def play(self, ai_circle, ai_cross):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # ai が着手を行うかどうかを判定する
        if (self.turn == Marubatsu.CIRCLE and ai_circle) or \
           (self.turn == Marubatsu.CROSS and ai_cross):
            x, y = ai1(self)
        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("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

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

```diff_python
-def play(self, ai_circle):
+def play(self, ai_circle, ai_cross):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # ai が着手を行うかどうかを判定する
-       if self.turn == Marubatsu.CIRCLE and ai_circle:
+       if (self.turn == Marubatsu.CIRCLE and ai_circle) or \
+          (self.turn == Marubatsu.CROSS and ai_cross):
            x, y = ai1(self)
        else:
            # キーボードからの座標の入力
以下同じなので省略
```
</details>

:::note
[以前の記事](https://qiita.com/ysgeso/items/1fdd19037bbf64082a42#%E8%AB%96%E7%90%86%E6%BC%94%E7%AE%97%E5%AD%90%E3%81%AE%E5%84%AA%E5%85%88%E9%A0%86%E4%BD%8D) で説明したように、__and 演算子__ の方が、__or 演算子__ より __優先順位が高い__ ので、上記のプログラムの 9、10 行目の条件式は、下記のプログラムのように __`()` を省略__ することが __できます__。ただし、下記のプログラムのように記述すると、__条件式で行う計算__ が __わかりづらくなる__ ので、上記のプログラムでは記述する __必要のない__ `()` を __記述__ しています。必要がないと思った方は `()` を削除しても構いません。

```python
        if self.turn == Marubatsu.CIRCLE and ai_circle or \
           self.turn == Marubatsu.CROSS and ai_cross:
```
:::


下記のプログラムを実行すると、__× を `ai1`__ が、__〇 を人間__ が __担当__ して 〇×ゲームを遊ぶことができます。下記の実行結果は 0,0、1,1、2,2 の順でテキストボックスに入力した場合で、__`ai1`__ は、__アルゴリズムの通り__ に (1, 0)、(2, 0) の順で着手を行い、〇 の勝利となります。

```python
mb.play(False, True)
```
実行結果
```
Turn o
...
...
...

Turn x
O..
...
...

Turn o
oX.
...
...

Turn x
ox.
.O.
...

Turn o
oxX
.o.
...

winner o
oxx
.o.
..O
```

下記は、__残り__ の「__両方とも人間が担当__」、「__〇 を `ai1` が担当__」、「__両方とも `ai1` が担当__」する場合の __確認__ するプログラムです。長くなるので実行結果は省略します（github のほうでは実際に実行しています）が、正しく実行されることが確認できます。

```python
mb.play(False, False)
mb.play(True, False)
mb.play(True, True)
```

### 仮引数の改良

上記のプログラムでは、__2 つ__ の __仮引数__ __`ai_circle`__ と __`ai_cross`__ によって __それぞれのマーク__ を人間か `ai1` の __どちらが担当__ するかを __判定__ していましたが、これを __1 つの仮引数__ に __まとめる__ ことができます。その場合は、__list__ を使って 〇 と × を __どちらが担当__ するかを __指定__ し、list の __0 番の要素__ が __〇__ を、__1 番の要素__ が  __×__  を __`ai1`__ が __担当するかどうか__ を表します。

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

* __1 行目__：__仮引数__ を __`is_ai` のみ__ に修正する
* __9、10 行目__：__`is_ai[0]`__ が __〇__ を、__`ai[1]`__ が __×__  を、__`ai1`__ が __担当するかどうか__ を __表す__ ので、__`ai_circle`__ を __`is_ai[0]`__ に、__`ai_cross`__ を __`is_ai[1]`__ に __修正__ する

```python
 1  def play(self, is_ai):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.status == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          print(self)
 8          # ai が着手を行うかどうかを判定する
 9          if (self.turn == Marubatsu.CIRCLE and is_ai[0]) or \
10             (self.turn == Marubatsu.CROSS and is_ai[1]):
11              x, y = ai1(self)
12          else:
13              # キーボードからの座標の入力
以下同じなので省略
```

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

```python
def play(self, is_ai):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # ai が着手を行うかどうかを判定する
        if (self.turn == Marubatsu.CIRCLE and is_ai[0]) or \
           (self.turn == Marubatsu.CROSS and is_ai[1]):
            x, y = ai1(self)
        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("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

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

```diff_python
-def play(self, ai_circle, ai_cross):
+def play(self, is_ai):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # ai が着手を行うかどうかを判定する
-       if (self.turn == Marubatsu.CIRCLE and ai_circle) or \
+       if (self.turn == Marubatsu.CIRCLE and is_ai[0]) or \
-          (self.turn == Marubatsu.CROSS and ai_cross):
+          (self.turn == Marubatsu.CROSS and is_ai[1]):
            x, y = ai1(self)
        else:
            # キーボードからの座標の入力
以下同じなので省略
```
</details>


実行結果は省略しますが、下記のプログラムで、人間と `ai1` の __4 通りの組み合わせ__ で対戦を行うことができることを確認できます。

```python
mb.play([False, False])
mb.play([True, False])
mb.play([False, True])
mb.play([True, True])
```

__修正前__ で記述していた、__`mb.play(False, False)`__ と __比較__ して、__`[` と `]`__  を __記述する必要__ があるので、この修正は __改良になっていない__ と __思う人__ がいる __かもしれない__ ので説明します。

この __修正__ は、__キーワード引数__ を使った場合に __有効__ となる __修正__ です。[以前の記事](https://qiita.com/ysgeso/items/c5dbe62b329f7936181a#%E4%BD%8D%E7%BD%AE%E5%BC%95%E6%95%B0%E3%81%A8%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89%E5%BC%95%E6%95%B0)で説明したように、__関数呼び出し__ の際に __複数の実引数__ を __記述__ した場合、それぞれの __実引数の意味__ が __分かりづらく__ なりがちですが、__キーワード引数__ を使って __実引数を記述__ することで、その __問題を緩和__ することができます。例えば、下記の 1 行目と 2 行目は __修正前__ の `play` に対して __同じ処理を行う__ プログラムですが、__2 行目__ の方が __わかりやすい__ と思いませんか？

```python
mb.play(True, False)
mb.play(circle_ai=True, cross_ai=False)
```

__キーワード引数__ の __欠点__ は、__キーワード__ を __記述する必要__ があるため、__実引数の数__ が __多くなる__ と、__記述が長くなってしまう__ 点にあります。先ほどの __修正__ は、__実引数__ を __1 つにまとめる__ ことで、__キーワード引数の記述__ を __短くする__ ための __修正__ です。下記のプログラムの 1 行目と 2 行目を比べて、__記述の量__ と、__どちらがわかりやすいか__ を __確認__ して下さい。

```python
mb.play(circle_ai=True, cross_ai=False)
mb.play(is_ai=[True, False])
```

:::note
人によって __何がわかりやすいか__ は __異なる__ ので、修正前の方がわかりやすいと思う人がいるかもしれません。そのような場合は、修正前のプログラムを採用して下さい。
:::

# ランダムなマスに着手を行う AI

`ai1` は簡単に実装できますが、全く強くはありませんし、__同じ局面__ であれば、毎回 __同じ着手しか行わない__ ので、対戦してもあまり面白くはありません。そこで、次は、簡単に実装できる AI として、下記のようなアルゴリズムで着手を行う AI を実装することにします。

* 空いているマスの中から __ランダム__ に 1 つを __選択する__

## 合法手の計算

一般的に、ゲームでプレイヤーがとることができる行動のことを __合法手__（legal move）と呼びます。ランダムなマスに着手を行うためには、__合法手__ の __一覧__ を __計算__ する必要があります。合法手の一覧の計算は、__今後の AI__ を __作成__ する際にも __必要__ になるので、`Marubatsu` クラスに __合法手の一覧__ を __計算__ する __メソッドを定義__ して利用することにします。

__名前__：__合法手__ を __計算__（calculate）するので、__`calc_legal_moves`__[^1]という名前にする
__処理__：__合法手の一覧__ を __計算__ する
__入力__：なし
__出力__：合法手は __(x, y)__ という __tuple で表現__ し、__合法手の一覧__ を __要素__ として持つ __list を返す__

下記のプログラムは、このメソッドの定義です。

* __2 行目__：計算した __合法手の一覧__ を __代入__ する __`legal_moves`__ という __ローカル変数__ に __空の list__  を __代入__ して __初期化__ する
* __3、4 行目__：__2 重の for 文__ によって、__すべてのマス__ に対する __繰り返し処理__ を行う
* __5、6 行目__：__(x, y)__ のマスが __空であるかどうか__ を __判定__ し、__空の場合__ は __`legal_moves`__ の要素に、__`(x, y)` を追加__ する
* __7 行目__：2 重の for 文による __繰り返し処理が終了__ した __時点__ で、__`legal_moves`__ に __合法手の一覧__ が __代入__ されているので、それを __返り値として返す__

```python
1  def calc_legal_moves(self):
2      legal_moves = []
3      for y in range(self.BOARD_SIZE):
4          for x in range(self.BOARD_SIZE):
5              if self.board[x][y] == Marubatsu.EMPTY:
6                  legal_moves.append((x, y))
7      return legal_moves
8
9  Marubatsu.calc_legal_moves = calc_legal_moves
```

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

```python
def calc_legal_moves(self):
    legal_moves = []
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            if self.board[x][y] == Marubatsu.EMPTY:
                legal_moves.append((x, y))
    return legal_moves

Marubatsu.calc_legal_moves = calc_legal_moves
```
</details>

[以前の記事](https://qiita.com/ysgeso/items/44884dcf168aab949508#for-%E6%96%87%E3%81%A7-dict-%E3%81%AE%E3%82%AD%E3%83%BC%E3%81%A8%E5%80%A4%E3%81%AE%E4%B8%A1%E6%96%B9%E3%82%92%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%99%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%97) で説明したように、__tuple を記述__ する際に、__基本的__ には __外側__ の __`()` を省略できますが__、省略すると __意味が曖昧なる場合__ は __省略__ することは __できません__。

上記のプログラムの 6 行目を下記のプログラムのように記述すると、__`(x, y)`__ という 1 つの tuple __ではなく__、__`x` と `y`__ という __2 つの変数__ が実引数に記述されるという __意味になる__ ため、__tuple__ の __外側の `()` を省略__ することは __できません__。

:::note alert
上記のプログラムを、下記のように記述することはできない。
```python
legal_moves.append(x, y)
```
:::

下記のプログラムは、`calc_legal_moves` を使って、ゲームの __開始直後__ に __合法手の一覧を表示__ した場合と、__(0, 0) に着手を行った直後__ に __合法手の一覧を表示__ した場合です。実行結果から、2 行目では __すべてのマス__ の座標が、4 行目では __(0, 0) を除いたマス__ の座標が __合法手の一覧__ として __計算される__ ことが確認できます。

```python
mb = Marubatsu()
print(mb.calc_legal_moves())
mb.move(0, 0)
print(mb.calc_legal_moves())
```
実行結果
```
[(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
[(1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
```

## list 内包表記の中に if を記述する方法

上記のプログラムは、__list 内包表記の中__ に __if を記述__ することで、__簡潔に記述__ することができます。実際に __良く使われる__ ので、その方法について説明します。

### 入れ子になっていない for 文の場合

上記のプログラムは __入れ子__ になった __2 重の for 文__ が記述されているため __複雑__ なので、先に __簡単な__、下記のような for 文が __入れ子になっていない例__ を説明します。

下記のプログラムは、以下のような処理を行うプログラムです。

* __1 行目__：`a` を空の list で初期化する
* __2 行目__：for 文で、0 から 10 未満までの整数を `i` に代入しながら繰り返し処理を行う
* __3、4 行目__：`i % 2 == 0` によって、`i` を 2 で割った余りが `0` である、すなわち __`i` が偶数であるかどうか__ を __判定__ し、__偶数の場合__ のみ __`a`__ の __要素__ に __`i` を追加__ する

実行すると、`0` から `10` 未満の __偶数__ を __要素__ として持つ __list__ が `a` に代入されます。

```python
a = []
for i in range(10):
    if i % 2 == 0:
        a.append(i)
print(a)
```
実行結果
```
[0, 2, 4, 6, 8]
```

上記のプログラムは、3 行目の __if 文がなければ__、list 内包表記を使って __`a = [i for i in range(10)]`__ のように記述できます。

上記のプログラムのように、for 文の __ブロックの中__ に __if 文が記述__ されており、その __条件式が `True` の場合__ に `a` に __要素を追加__ する処理を行うプログラムは、下記のプログラムのように __list 内包表記__ を使って __簡潔に記述__ することができます。

```python
a = [i for i in range(10) if i % 2 == 0]
print(a)
```
実行結果
```
[0, 2, 4, 6, 8]
```

一見すると、list 内包表記への変換がややこしく思えるかもしれませんが、__以下の手順__ を覚えると __簡単__ に list 内包表記に __変換できるようになる__ と思います。

__手順 1__：for 文と if 文を抜き出す

```python
for i in range(10):
    if i % 2 == 0:
```

__手順 2__：上記の __`:` を削除__ して __1 行で並べる__

```python
for i in range(10) if i % 2 == 0
```

__手順 3__：__上記__ の __先頭__ に __`append` の実引数を記述__ し、全体を __`[]` で囲う__

```python
[ i for i in range(10) if i % 2 == 0 ]
```

### 入れ子になっている for 文の場合

下記は __`calc_legal_moves`__ の中の、__`legal_moves` を計算する部分__ のプログラムです。

```python
legal_moves = []
for y in range(self.BOARD_SIZE):
    for x in range(self.BOARD_SIZE):
        if self.board[x][y] == Marubatsu.EMPTY:
            legal_moves.append((x, y))
```

上記のプログラムような、__入れ子__ になっている __for 文__ の場合は、4 行目の if 文がなければ、[以前の記事](https://qiita.com/ysgeso/items/f7c98c1e159e61755174#%E5%85%A5%E3%82%8C%E5%AD%90%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F-for-%E6%96%87%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8B-list-%E5%86%85%E5%8C%85%E8%A1%A8%E8%A8%98) で説明したように、下記プログラムのような __list 内包表記で記述__ できます。

```python
legal_moves = [(x, y) for y in range(self.BOARD_SIZE) for x in range(self.BOARD_SIZE)]
```

4 行目の __if 文__ を __考慮に入れた場合__ は、__先程と同様__ に、__最後__ に __if の記述を追加__ して、下記のプログラムのように記述することができます。なお、長いので途中で改行しました。

この修正も、__先ほど__ 説明した場合と __同様__ に、__for から if まで__ の行の __`:` を削除__ し、__`[` と `]` の中__ に __記述__ すれば良いので、手順は難しくはないでしょう。

```python
legal_moves = [(x, y) for y in range(self.BOARD_SIZE) for x in range(self.BOARD_SIZE)
                      if self.board[x][y] == Marubatsu.EMPTY]
```

下記は、list 内包表記を使って `calc_legal_moves` を修正したプログラムです。なお、大部分が修正されているので、修正箇所は省略します。

```python
def calc_legal_moves(self):
    legal_moves = [(x, y) for y in range(self.BOARD_SIZE) 
                          for x in range(self.BOARD_SIZE)
                          if self.board[x][y] == Marubatsu.EMPTY]
    return legal_moves

Marubatsu.calc_legal_moves = calc_legal_moves
```

下記は、修正した `calc_legal_moves` が正しく動作するかどうかを確認するプログラムです。実行結果から、正しく動作することが確認できます。

```python
mb = Marubatsu()
print(mb.calc_legal_moves())
mb.move(0, 0)
print(mb.calc_legal_moves())
```
実行結果
```
[(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
[(1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
```

本記事では、この記述を採用しますが、分かりづらいと思った方は __無理に__ list 内包表記を __使う必要はありません__ ので、分かりやすいと思ったほうを採用して下さい。 

### else がある場合の記述方法

今回の記事では利用しませんが、下記のプログラムのように、__else が記述__ されている場合でも __list 内包表記__ で __記述__ することが __できます__。なお、下記のプログラムは、__奇数__ を __負の数に変換__ した、__0 から 10 未満の整数__ を __要素__ として持つ __list__ を __`a` に代入__ します。

```python
a = []
for i in range(10):
    if i % 2 == 0:
        a.append(i)
    else:
        a.append(-i)
print(a)
```
実行結果
```
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9]
```

このプログラムは、list 内包表記を使って、下記のように記述できます。

else がない場合と異なり、__if や else__ などは、__for より前__ に記述する必要があります。

```python
a = [i if i % 2 == 0 else -i for i in range(10) ]
print(a)
```
実行結果
```
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9]
```

上記のプログラムで、for の前に記述する `i if i % 2 == 0 else -i` の部分は、[以前の記事](https://qiita.com/ysgeso/items/ed1eb1cacf9a3e7dfa29#%E4%B8%89%E9%A0%85%E6%BC%94%E7%AE%97%E5%AD%90) で説明した __三項演算子__ です。

```python
a = []
for 変数 in 反復可能オブジェクト:
    if 条件式:
        a.append(式1)
    else:
        a.append(式2)
```

従って、上記のプログラムは、list 内包表記を使って、下記のように記述できます。

```python
a = [式1 if 条件式 else 式2 for 変数 in 反復可能オブジェクト]
```

## list からのランダムな要素の選択

Python には、__ランダムな処理__ を行うための __`random`__ という __モジュール__ があり、その中に __`choice`__ という、__反復可能オブジェクト__ の __要素の中__ から __ランダム__ に __一つ選択__（choice）した __要素を返す__ という __関数が定義__ されています。この `choice` を使うことによって、__合法手の一覧の中__ から __ランダム__ に __合法手を 1 つ選択__ することができます。

下記は、`legal_moves` の要素をランダムに一つ選択して表示するプログラムです。

* __1 行目__：__`random` モジュール__ から __`choice` 関数__ を __インポート__ する
* __3 行目__：__`legal_moves`__  に __合法手の一覧__ を __代入__ する
* __4 行目__：__`choice`__ を使って、__`legal_moves`__ の __要素__ を __ランダムに選択__ して __表示__ する


```python
from ramdom import choice

legal_moves = mb.calc_legal_moves()
print(choice(legal_moves))
```

実行結果（__実行結果はランダム__ なので __下記とは異なる場合__ があります）
```python
(1, 0)
```

`random` モジュールの詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/random.html?highlight=random#module-random

`choice` の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/random.html?highlight=random#random.choice

## `ai2` の定義

合法手の一覧の中からランダムな合法手を選択する方法が分かったので、`ai2` は以下のようなプログラムで定義することができます。

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

下記のプログラムを実行することで、ゲーム開始直後に、__`ai2`__ を使って __ランダムな合法手__ を __表示__ することができます。

```python
mb = Marubatsu()
print(ai2(mb))
```

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

## `play` メソッドの改良

先程定義した `play` メソッドは、〇 と × を __`ai1` が担当するかどうか__ を選択できるように実装したので、__`ai2` が担当するかどうか__ を選択できるようにするためには、`play` メソッドの下記の 4 行目を __`x, y = ai2(self)`__ のように修正する必要があります。

```python
# 〇の手番で、ai_circle が True の場合は ai が着手を行う
if (self.turn == Marubatsu.CIRCLE and is_ai[0]) or \
   (self.turn == Marubatsu.CROSS and is_ai[1]):
    x, y = ai1(self)
```

しかし、そのように修正すると、__今度は__ `play` メソッドで __`ai1` を選択できなく__ なってしまいます。そこで、__`play`__ メソッドを __改良__ し、〇 と × の着手を __人間__ または、__指定した AI の処理を行う関数__ が担当するかどうかを __選択できる__ ように修正することにします。

[以前の記事](https://qiita.com/ysgeso/items/cd3e1c0731b859bf5e2c#%E5%A4%89%E6%95%B0%E3%81%AB%E9%96%A2%E6%95%B0%E3%82%92%E7%9B%B4%E6%8E%A5%E4%BB%A3%E5%85%A5%E3%81%99%E3%82%8B%E4%BE%8B)で説明したように、Python では、__変数に関数を代入__ することができます。また、__関数を代入した変数__ は、__関数と同じ機能を持つ__ ようになるため、下記のプログラム[^2]の 5 行目のように、__関数呼び出し__ を行うことができるようになります。

```python
def add(x, y):
    return x + y

a = add
print(a(1, 2))
```
実行結果
```
3
```

この性質から、__`play`__ メソッドの __仮引数__ に、__AI の処理__ を行う __関数を代入__ することで、`play` メソッドの __ブロックの中__ で __指定した AI__ の __処理を行う関数__ を __呼び出す__ ことができるようになります。その際に、__人間__ が __手番を担当__ する場合の __データ__ を __どのように記述するか__ を __決める__ 必要があります。本記事では、__人間__ が手番を __担当__ するということは、__AI__ が __存在しない__ ことなので、__データが存在しない__ ことを表す __`None` で表現__ することにします。

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

* __1 行目__：__仮引数__ が、__AI の処理__ を行う __関数を代入__ するように __変更__ されたので、__仮引数の名前__ を名前を __`ai`__ に __変更__ する
* __9、10 行目__：__〇 の手番__ で、__なおかつ__、__人間の担当ではない__（`ai[0] is not None`）場合は、__`ai[0]`__ に __AI の処理__ を行う __関数が代入__ されているので、__`ai[0](self)`__ を呼び出して __着手を計算__ し、その座標を __`x` と `y` に代入__ する
* __11、12 行目__：__× の手番__ で、__なおかつ__、__人間の担当ではない__（`ai[1] is not None`）場合に、9、10 行目と __同様の処理__ を行う

```python
 1  def play(self, ai):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.status == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          print(self)
 8          # ai が着手を行うかどうかを判定する
 9          if self.turn == Marubatsu.CIRCLE and ai[0] is not None:
10              x, y = ai[0](self)
11          elif self.turn == Marubatsu.CROSS and ai[1] is not None:
12              x, y = ai[1](self)
13          else:
14              # キーボードからの座標の入力
以下同じなので省略
```

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

```python
def play(self, ai):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # ai が着手を行うかどうかを判定する
        if self.turn == Marubatsu.CIRCLE and ai[0] is not None:
            x, y = ai[0](self)
        elif self.turn == Marubatsu.CROSS and ai[1] is not None:
            x, y = ai[1](self)
        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("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

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

```diff_python
-def play(self, is_ai):
+def play(self, ai):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # ai が着手を行うかどうかを判定する
-       if (self.turn == Marubatsu.CIRCLE and is_ai[0]) or \
-          (self.turn == Marubatsu.CROSS and is_ai[1]):
-          x, y = ai(self)
+       if self.turn == Marubatsu.CIRCLE and ai[0] is not None:
+           x, y = ai[0](self)
+       elif self.turn == Marubatsu.CROSS and ai[1] is not None:
+           x, y = ai[1](self)
        else:
            # キーボードからの座標の入力
以下同じなので省略
```
</details>

## 改良した `play` メソッドの動作の確認

`play` メソッドが __正しく動作するか__ どうかを __確認__ するために、人間、`ai1`、`ai2` の __すべての組み合わせ__ で __対戦__ を行うことにします。以下、__対戦の組み合わせ__ を、__〇 の担当__ を __先に記述__ して、__`ai` VS `ai1`__ のように __表記__ することにします。

### `ai1` VS `ai1`

下記は、__`ai1` VS `ai1`__ で対戦を行うプログラムです。__`ai1`__ は __ランダムな着手__ は __行わない__ ので、__常に__ 対戦 __結果__ は __同じ__ になります。そのことを確認したい方は、下記のプログラムを __何度か実行__ し、__実行結果__ が __常に同じになる__ ことを __確認__ して下さい。

```python
mb.play(ai=[ai1, ai1])
```
実行結果
```
Turn o
...
...
...

Turn x
O..
...
...

Turn o
oX.
...
...

Turn x
oxO
...
...

Turn o
oxo
X..
...

Turn x
oxo
xO.
...

Turn o
oxo
xoX
...

winner o
oxo
xox
O..
```

### `ai1` VS `ai2`

下記は、__`ai1` VS `ai2`__ で対戦を行うプログラムです。__`ai2`__ は __ランダムな着手__ を行うので、__毎回__ 対戦 __結果__ は __変わります__。一方、__`ai1`__ は、__必ず__ 左上のマスから __順番__ に __空いているマスを探し__、__最初に見つかったマス__ に __着手__ を行います。下記の実行結果では、実際に __そのような方針__ で __〇 を担当__ する __`ai1`__ が __着手を行っている__ ことが __確認__ できます。

```python
mb.play(ai=[ai1, ai2])
```
実行結果（実行結果はランダムなので下記とは異なる場合があります）
```
Turn o
...
...
...

Turn x
O..
...
...

Turn o
o..
..X
...

Turn x
oO.
..x
...

Turn o
ooX
..x
...

Turn x
oox
O.x
...

Turn o
oox
o.x
X..

Turn x
oox
oOx
x..

Turn o
oox
oox
xX.

winner o
oox
oox
xxO

```

### その他の対戦

下記は、__残りの組み合わせ__ で __対戦を行う__ プログラムです。__実行結果__ は長くなるので __省略__ しますが、__以下の点に注目__ して __実際に何度か対戦__ してみて下さい。

* __`ai2` VS `ai1`__：〇 と × の __担当が入れ替わった__ 点を除けば、__`ai1` VS `ai2`__ の場合と __同様__
* __`ai2` VS `ai2`__：__すべて__ の着手で __ランダムな着手__ が行われる
* __`人間` VS `ai1`__、__`ai1` VS `人間`__：`ai1` の着手が、`ai1` のアルゴリズムで行われる
* __`人間` VS `ai2`__、__`ai2` VS `人間`__：`ai2` の着手が、ランダムに行われる
* __`人間` VS `人間`__：人間どうしの対戦が正しく行われる

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

## `play` メソッドの if 文の処理をまとめる

先程の `play` メソッドの下記の部分は、__2、3 行目__ と __4、5 行目__ に __同じような処理__ が記述されているので、__冗長__ だと思いませんか？

```python
1  # ai が着手を行うかどうかを判定する
2  if self.turn == Marubatsu.CIRCLE and ai[0] is not None:
3      x, y = ai[0](self)
4  elif self.turn == Marubatsu.CROSS and ai[1] is not None:
5      x, y = ai[1](self)
6  else:
7      略
```

2、3 行目と 4、5 行目の __違い__ は、__前半__ の __`self.turn ==`__ の部分と、__その後__ の __`ai[0]`__、__`ai[1]`__ の部分です。__`ai[0]` と `ai[1]`__ の __違い__ は、__インデックスの番号__ で、その __番号__ は __前半__ の __`self.turn ==`__  の部分 __で決まる__ ので、下記のプログラムのように、__先に__ インデックスの __番号__ を __if 文で計算する__ ことで、プログラムを __まとめる__ ことができます。

```python
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
    x, y = ai[index](self)
else:
    略
```

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

```python
def play(self, ai):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self)
        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("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```

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

```diff_python
def play(self, ai):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # ai が着手を行うかどうかを判定する
-       if self.turn == Marubatsu.CIRCLE and ai[0] is not None:
-           x, y = ai[0](self)
-       elif self.turn == Marubatsu.CROSS and ai[1] is not None:
-           x, y = ai[1](self)
        # 現在の手番を表す ai のインデックスを計算する
+       index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ai が着手を行うかどうかを判定する
+       if ai[index] is not None:
+           x, y = ai[index](self)
        else:
            # キーボードからの座標の入力
以下同じなので省略
```
</details>

本当は `play` メソッドを修正したので正しく動作するかどうかを確認する必要がありますが、長くなるので省略します。余裕がある人は各自で確認しておいてください。

# `ai1` の別の実装方法

__`ai1`__ は、__`calc_legal_moves`__ を使って、下記のプログラムのように __簡潔に記述__ できます。

__`calc_legal_moves`__ は、__合法手の一覧__ を、__`ai1` と同様__ に、__左上のマス__ から __順番に調べている__ ので、見つかった __合法手の一覧__ の __先頭__ の __合法手__ が、__`ai1`__ が __選択する合法手__ になります。従って、下記のプログラムの 3 行目のように、__合法種の一覧__ を表す __list__ の __先頭の要素__ である __`legal_moves[0]`__ を __返す__ ことで `ai1` を実装できます。

```python
def ai1(mb):
    legal_moves = mb.calc_legal_moves()
    return legal_moves[0]
```

なお、__元の `ai1`__ は、__空のマス__ が __見つかった時点__ で __処理が終了__ しますが、上記のプログラムは、__すべての合法手を見つけてから__ 、その先頭の __合法手を返す__ 処理を行うので、上記のプログラムの方が、元の `ai1` よりも __処理に時間がかかる__ という __欠点__ があります。

ただし、__`ai1`__ の __アルゴリズム__ の __処理にかかる時間__ は __短い__ ので、どちらを選択しても __大きな違いはない__ ので、どちらを選んでも良いと思います。

本記事では元の `ai1` の定義を採用することにします。

# 今回の記事のまとめ

今回の記事では、__簡単に実装__ できる __AI の処理__ を行う __関数__ を __2 種類実装__ し、__`play`__ メソッドを __改良__ して、〇 と × を、__人間__ または __任意の AI が担当__ できるように __改良__ しました。

作成した AI の __アルゴリズム__ は以下の通りです。

| 関数名 | アルゴリズム |
|:-:|:--|
| __`ai1`__  | 左上から順に空いているマスを探し、最初に見つかったマスに着手する |
| __`ai2`__  | ランダムなマスに着手する  |

今回作成した AI はどちらも __非常に弱い__ ので、次回の記事からは、どのようにすれば __強い AI__ を __実装__ できるかについて説明します。

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

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

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

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

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

AI に関連するプログラムは、`Marubatsu` クラスとは __別の処理__ を行うので、__ai.py に記述する__ ことにします。以下のリンクは、今回の記事で作成した ai.py です。

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

# 次回の記事

https://qiita.com/ysgeso/items/5725504c97a4490fbadb

[^1]:何かを計算する関数の名前の一部として、calculate の略である calc が良く使われます
[^2]:以前の記事では `sum` という変数に `add` を代入していましたが、`sum` という名前の組み込み関数が存在するので、上記のプログラムでは `a` に代入しています
[^3]:ランダムなので、同じ局面で、偶然前回と同じ着手を `ai2` が行う場合があります