Pythonで〇×ゲームのAIを一から作成する　その40　AI の強さの評価方法

# 目次と前回の記事

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

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

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

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

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

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

## これまでに作成した AI

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

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

# AI の強さの評価方法

本記事ではさまざまな 〇×ゲームの AI を作成しますが、その際に、作成した AI の __強さを評価__ する必要があります。何らかの競技で強さを評価する __一般的な方法__ として、__複数回__ の __対戦__ を行い、__勝率を比較__ するという方法があります。本記事でも __作成した AI どうし__ で __複数回__ の __対戦__ を行うことで AI の __強さを評価__ することにします。

## 対戦の回数

__対戦の回数__ が 1 回や数回のように __少ない__ 場合は、__弱い AI__ が 強い AI に __偶然勝ち越す__ ような、__実際の強さ__ とは __異なる結果__ がでる __可能性__ が __高く__ なりますが、対戦の __回数を増やせば__ 増やすほど、__勝率__ が __実際の AI の強さ__ を __より正確__ に __反映する__ ようになります。このような法則の事を __大数の法則__ と呼びます。大数の法則の具体例として良く挙げられる例として、投げた時に __表と裏__ がそれぞれ __50 % の確率で出る__ コインを何回か投げた時に、投げる __回数が少なければ__ 表が出る割合が __50%__ から大きく __かけ離れる場合がある__ が、投げる __回数を多くすれば__ するほど表が出る割合が __50 % に近づいていく__ というものがあります。

参考までに大数の法則の Wikipedia へのリンクを下記に挙げておきます。

https://ja.wikipedia.org/wiki/%E5%A4%A7%E6%95%B0%E3%81%AE%E6%B3%95%E5%89%87

__大数の法則__ から、__AI どうし__ の対戦の __回数を多くすれば__ するほど、__計測された勝率__ が、__実際の__ AI どうしの __強さ__ を __より正確に表す__ ようになりますが、__対戦回数__ が __一定以上多くなる__ と __精度__ がほとんど __上がらなく__ なります。また、__対戦回数__ を __多くすれば__ するほど __対戦__ に __時間がかかる__ ようになるので、本記事では AI どうしの __対戦__ を __一万回__ 行うことにします。たたし、複雑な処理を行う AI のどうしの対戦などで、__一万回の対戦__ を行うと __時間がかかりすぎる__ 場合は、__1 分程度__ で __終わる__ ような __回数の対戦__ を行うことにします。

:::note warn

以下に、AI どうしの __対戦回数__ に関する __方針__ をまとめます。

* AI どうしの __対戦の回数__ は、__原則__ として __一万回__ 行う
* 一万回の対戦に __時間がかかりすぎる場合__ は、__1 分で行える回数__ の対戦を行う
:::

## AI のランダム性

上記で複数回の対戦を行うと説明しましたが、対戦する __双方__ の AI の __アルゴリズム__ に __ランダム性がなければ__ 、__複数回の対戦__ を行う __必要__ は __ありません__。その理由は、__ランダム性のない__ アルゴリズムの AI は、__同じ局面__ で必ず __同じ着手__ を行うからです。実際に、[前回の記事](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#ai1-vs-ai1)で説明したように、__ランダム性のない__ __`ai1` どうしの対戦__ では、__常に同じ対戦結果__ になります。そのため、__ランダム性のない__ アルゴリズムの __AI どうしの対戦__ から、AI の __強さの評価__ を行うことは __できません__。その場合は __1 回の対戦__ のみを行い、__勝敗__ は __参考結果__ とします。

## 対戦の組み合わせ

__複数__ の __AI__ の __強さを評価__ する方法は一つではありません。そこで、__それぞれの方法__ について __考察__ し、__どの方法を選択するか__ を __決める__ ことにします。

### 総当たり方式

__複数__ の __AI__ の __強さを評価__ する __最も正確な方法__ は、__すべての AI どうし__ で __対戦__ を行う、__総当たり方式__（リーグ戦）で __対戦を行う__ という方法です。〇× ゲームは、__〇 の手番__ と __× の手番__ で __有利不利__ が __大きく異なる__ ゲームなので、例えば __`ai1` と `ai2`__ の __強さを評価__ する場合は、__〇 と ×__ の __担当__ を __入れ替えた__、__`ai1` VS `ai2`__ と __`ai2` VS `ai1`__ の __両方の対戦__ を __行う必要__ があります[^1]。さらに、〇 と × で __どれくらい有利不利があるか__ を __調べる__ ために、 __`ai1` VS `ai1`__ のような、__同じ AI どうし__ の対戦を __含めた場合__ は、__$n$ 個__ の AI どうしの __総当たり戦__ の __組み合わせ__ は、$n ^ 2$ 通り になります。そのため、作成した __AI の数__ が __増えていく__ に従い、対戦の __組み合わせ__ が __急激に増える__ ため、AI どうしの __対戦__ を行うことが __困難__ になります。例えば __AI の数__ が __100__ の場合は、__対戦の組み合わせ__ は $100^2$ = __1 万__ 通りになります。

:::note warn
__総当たり方式__ の __利点と欠点__ は以下の通りです。

__利点__：__最も正確__ に __強さを評価__ できる
__欠点__：AI の __数__ が __増える__ と、対戦の __組み合わせ__ が __急激に増える__
:::

実際に総当たり方式で AI の評価を行うと、__対戦に時間がかかりすぎるよう__ になってしまうため、本記事では __総当たり方式__ は __採用しない__ ことにします。

### トーナメント方式

高校野球のような、__何千以上の多くの__ チームの __優劣を決める方法__ として、__トーナメント方式__ があります。トーナメント方式では、__1 回の試合__ で __1 つ__ のチームが __敗退__ し、__最終的__ には __1 つのチーム__ が __優勝する__ ので、__n 個__ のチームでトーナメント方式で試合を行うと、全部で __n - 1 回試合__ を行います。従って、チーム数が __増えても__ 試合数が __急激に増える__ ことは __ありません__。そのため、トーナメント方式で AI どうしの強さを評価すればよいと思う人がいるかもしれませんが、__以下の理由__ で本記事では __トーナメント方式__ は __採用しません__。

* トーナメント方式は、__あらかじめ__ 強さの評価を行いたい __AI__ が __すべて揃っている__ 場合は利用できるが、本記事で行うように、__新しい AI__ を __次々に作成していく__ ような場合では、新しい __AI を作成するたび__ に __トーナメント__ を __毎回やり直す__ 必要が生じてしまう
* 下記で具体例を示すように、トーナメント方式は、本当は __強い AI ではない__ が、__AI どうしの相性__ がよかったせいで __勝ち残ってしまう可能性__ がある

:::note warn
__トーナメント方式__ の __利点と欠点__ は以下の通りです。

__利点__：AI の __数__ が __増えても__、対戦の __組み合わせ__ が __急激に増えない__
__欠点__：新しい __AI が増える__ とトーナメントを __やり直す必要__ がある<br>　　　__最も強い__ AI が __優勝__ するとは  __限らない__
:::

#### 相性のせいで弱い AI がトーナメント方式で優勝する確率が高い例

__AI どうし__ の __相性__ のせいで、__強くない__ AI が __勝ち残る可能性が高い__ 具体例を紹介します。

__じゃんけんの AI__ で、下記の __4 つの AI__ を、下図の __トーナメント方式__ で戦わせた場合、__それぞれの AI__  が __優勝する確率__ を考えてみて下さい。なお、__あいこに__ なった場合は、__決着がつくまで__ 手を出し続けるものとします。

| AI の名前 | アルゴリズム |
|:-:|:-:|
| AI1  | 常にグーを出す  |
| AI2  | 常にパーを出す  |
| AI3  | 常にチョキを出す  |
| AI4  | ランダムに手を出す  |

<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/23d20235-397e-68d3-ff5e-6fe9df11046a.png" width="200">

__AI1 VS AI4__ は、AI1 の __勝ち__、__負け__、__あいこ__ の __確率__ は __いずれも 1/3__ ですが、__あいこの場合__ は __どちらかが勝つまで__ 手を出し続けるので、__AI1__ の __勝ち__ と __負け__ の __確率__ は __いずれも 50 %__ になります。これは、__AI2 VS AI4__ と __AI3 と AI4__ の場合も __同様__ です。従って、上図のトーナメントの __それぞれの対戦結果__ は以下のようになります。

* 1 回戦の __AI1 VS AI2__ は、グーに対してパーを出す __AI2__ が __100 % 勝つ__
* 1 回戦の __AI3 VS AI4__ は、__お互い__ の __勝率は 50 %__ となる
* 1 回戦で __AI3 が勝利__ した場合は、__AI2 VS AI3__ となり、チョキを出す __AI3__ が __100 % 勝つ__
* 1 回戦で __AI4 が勝利__ した場合は、__AI2 VS AI4__ となり、__お互い__ の __勝率は 50 %__ となる

従って、それぞれの AI の勝率は以下の表のようになり、__常にチョキを出す__ AI3 が __50 % の確率__ で __最も優勝確率が高く__ なり、__AI2__ と __AI4__ は __25 %__、__AI1__ は決して __優勝できません__。

| AI の名前 | 1 回戦の勝率 | 決勝の勝率 | 優勝確率 |
|:-:|:-:|:-:|:-:|
| AI1  | 0 %  | - | 0 % |
| AI2  | 100 %   | 相手が AI3 の場合は 0 %<br>相手が AI4 の場合は 50 % | 1 * 0.5 * 0 + 1 * 0.5 * 0.5 = 25 % |
| AI3  | 50 %  | 100 % | 0.5 * 1 = 50 % |
| AI4  | 50 % | 50 % | 0.5 * 0.5 = 25 % |

じゃんけんの __グー__、__チョキ__、__パー__ は __3 すくみ__ の関係なので、__優劣__ は __ありません__ が、上記の __トーナメント__ では、__グー__ を出す AI1 の __優勝確率__ は __0 %__、__パー__ を出す AI 2 の __優勝確率__ は __25 %__、__チョキ__ を出す AI3 の __優勝確率__ は __50 %__ のように、__偏った__ 優勝確率になります。

また、じゃんけんを遊んだことがあれば、上記の __トーナメント__ で最も __優勝確率が高い__、常にチョキを出す __AI3__ が __強いAIではない__ ことはすぐにわかるはずです。実際に __人間と AI3__ が __対戦__ した場合は、__AI3__ はすぐに __勝てなくなる__ はずです。一方、__人間__ とランダムな手を出す __AI4__ が __対戦__ した場合は、__お互い__ の __勝率は約 50 %__ になるので、__人間と対戦する場合__ は、__AI4 のほう__ が AI3 より __強い__ という、上記の __トーナメント__ と __異なる結果__ になります。

__トーナメント方式__ でこのようなことがおきるのは、__対戦相手の相性__ によって __強い AI__ が必ずしも __優勝する確率__ が __高くならない場合がある__ からです。

#### 総当たり方式の場合

参考までに、上記を __総当たり方式__ で対戦した場合の __勝率__ を以下の表で示します。表は、__左の列__ の AI __からみた勝率__ です。また、__AI1__、__AI2__、__AI3__ は __同じ AI どうし__ で __対戦__ すると永遠に __決着がつかない__ ので、それらの組み合わせの対戦は __省略__ します。

|  | AI1 | AI2 | AI3 | AI4 | 平均 |
|:-:|:-:|:-:|:-:|:-:|:-:|
| __AI1__  |   | 0 %  | 100 %  | 50 %  | 50 %  |
| __AI2__  | 100 %  |   | 0 % | 50 %  | 50 %  |
| __AI3__  | 0 %  | 100 %  |   | 50 %  | 50 %  |
| __AI4__  | 50 %  | 50 %  | 50 %  | 50 %  | 50 %  |

表から、__総当たり方式__ の場合は __すべての AI__ の __平均勝率が 50 %__ になってしまうので、総当たり方式でも正しく __AI の強さが評価できない__ と __思う人がいる__ かもしれませんが、__そうではありません__。確かに、上記の 4 つの AI どうしの対戦の中には、__どちらかが 100 % 勝利する__ という __極端な相性が存在__ しますが、この __4 つの AI どうし__ で __ランダムな組み合わせ__ で __対戦__ を行うと、__どの AI も勝率が 50 % になる__ ことは __間違ってはいない__ のです。また、__すべて__ の __組み合わせ__ で __対戦が行われる__ ので、__個々__ の __AI どうしの相性__ を __確認__ することができます。

なお、総当たり方式やトーナメント方式に限らず、__すべての場合で共通する__ ことですが、__参加した AI の中__ での __強さを測る__ こと __しかできない__ という __限界がある__ 点に __注意が必要__ です。分かりやすい例でいうと、__小学生__ の野球の __総当たり戦で優勝__ したからといって、そのチームが __中学生__ の野球の __総当たり戦で優勝__ できることはほとんど __あり得ないない__ でしょう。__本当の強さ__ を __測る__ ためには、__多種多様な相手__ と __対戦__ することが __重要__ になるのです。

### 基準となる AI との比較

他の方法として、__基準となる AI__ を用意し、__新しい AI__ を作成した際に、__基準となる AI と対戦__ させた結果で __AI の強さを評価__ するという方法が考えられます。この方法は、新しい AI を作成する際に、__基準となる AI__  と __対戦__ させる __だけで良い__ ので、__評価に手間がかからない__ という __利点__ があります。一方で、__基準となる AI__ が __偏った性質__ を持つ場合に、__正しい評価__ が __行えない__ という __欠点__ があります。例えば、先程の例の、じゃんけんで __常にグーを出す__ ような AI を __基準としてはいけない__ ことは誰でもすぐに理解できるのではないかと思います。

これがベストな選択であると断言することはできませんが、__ランダムな着手__ を行う __`ai2`__ は、__ランダム__ という性質から、__あまり偏っていない__ AI であると __考えること__ ができます。そこで、本記事では __`ai2`__ を __基準となる AI__ とし、__新しい AI を作成__ するたびに __`ai2` と対戦__ させることで、作成した AI の __強さの評価__ を行うことにします。

なお、__`ai1`__ を基準となる AI として __選ばなかった理由__ の一つは、`ai1` の __アルゴリズム__ に __ランダム性がない__ からです。先程説明したように、__ランダム性のない AI__ を作成して __`ai1` と対戦__ させても __毎回同じ結果__ になってしまうため、__強さの評価__ を行うことは __できません__。

もう一つの理由は、__`ai1`__ の __アルゴリズム__ が、じゃんけんの __常に同じ手を出す AI__ と __同様__ に、__偏っている__ からです。また、本記事の最後で説明しますが、__`ai1` と `ai2`__ は、__客観的に考える__ と __`ai2` の方が強い__ と言えますが、__`ai1`__ は `ai2` に対して __相性が良い__ ので、__`ai1` と `ai2`__ を __対戦__ させると __`ai1` のほうが勝率が高く__ なってしまいます。

### 強い AI との比較

__基準となる AI__ との __比較__ は、__作成した AI__ が __弱いうちは有効__ ですが、AI が __ある程度以上強くなる__ と、基準となる AI と対戦しても、__勝率__ に __差が__ ほとんど __生じなく__ なります。

そのため、AI がある程度以上 __強くなった場合__ は、その時点までで __作成した強い AI どうし__ で __対戦__ することで、__AI の強さ__ を __評価する__ 必要が生じます。

そこで、本記事では、__新しい AI を作成__ した際に、基準となる AI だけでなく、__それまでに作成__ した AI の中で __最も強い__ と思われる __いくつかのAI との対戦__ も行うことにします。その際に、__どの AI と対戦__ させるかについては、__状況に応じて__ その都度 __判断__ することにします。

### 本記事で行う対戦の組み合わせ

本記事では、新しい AI を作成するたびに、下記の AI と対戦を行うことにします。

* ランダムな着手を行う __`ai2`__
* __それまでに作成__ した AI の中で __最も強い AI__ と思われるのうちの __いくつか__

なお、上記で説明した以外にも AI どうしの強さをよりよく評価できる方法は実際にあると思います。そのような方法を思いついた人がいれば、それを採用してもかまいません。

# AI どうしの対戦処理

AI の強さの評価を行う方法が決まったので、その方法に従って、__指定した AI どうし__ を __複数回対戦__ させて、その __通算成績__ を __集計__ する必要があります。例えば __`ai1` と `ai2`__ の __対戦__ を __2 回__ 行う場合は、下記のプログラムのように、__`play`__ メソッドを __for 文__ を使って __2 回呼び出せば良い__ のですが、__`play`__ メソッドを __実行__ しても、__勝敗結果__ は画面に __`winner o`__ のように __表示__ される __だけ__ なので、__このまま__ では __通算成績__ を __集計__ することは __できません__。

```python
from marubatsu import Marubatsu
from ai import ai1, ai2

mb = Marubatsu()

for _ in range(2):
    mb.play(ai=[ai1, ai2])
```

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

略

winner o
oox
oox
xxO

Turn o
...
...
...

略

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

## `play` メソッドの修正 1（対戦結果を返すようにする）

__プログラム__ で `play` メソッドの __対戦結果__ を __集計__ するためには、__`play`__ メソッドが、__対戦結果__ を __返り値__ として __返す__ ように __修正__ する必要があります。__対戦結果__ は、__`status` 属性__ に __代入__ されるので、下記のプログラムの 11 行目に __`return self.status` を追加__ することで、`play` メソッドが __対戦結果を返す__ ようになります。

```python
 1  def play(self, ai):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4
元と同じなので省略
 5
 6      # 決着がついたので、ゲーム盤を表示する
 7      print(self)
 8      # 勝敗結果を返り値として返す
 9      return self.status
10
11  Marubatsu.play = play
```

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

```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)
    return self.status

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

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

```diff_python
def play(self, ai):
    # 〇×ゲームを再起動する
    self.restart()

元と同じなので省略

    # 決着がついたので、ゲーム盤を表示する
    print(self)
    # 勝敗結果を返り値として返す
+   return self.status

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


下記のプログラムを実行し、__実行結果__ に __`winner: o`__ のような表示が行われることから、__`play`__ メソッドが __勝敗結果を返す__ ようになったことが __確認__ できます。

```python
for _ in range(2):
    print("winner: ", mb.play(ai=[ai1, ai2]))
```

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

```
Turn o
...
...
...

略

winner x
oox
oox
x.X

winner:  x
Turn o
...
...
...

略

winner o
ooO
x.x
...

winner:  o
```

:::note
__勝敗結果__ は `Marubatsu` クラスのインスタンスの __`status` 属性__ に __代入__ されるので、`play` メソッドを __修正しなくても__、下記のプログラムのように記述すれば良いと思った人がいるかもしれません。なお、実行結果は上記と同様なので省略します。

```python
for _ in range(2):
    mb.play(ai=[ai1, ai2])
    print("winner: ", mb.status)
```

確かに、__上記のプログラム__ でも __正しく動作__ しますが、上記のプログラムを __記述するため__ には、__`Marubatsu` クラス__ に、ゲームの __勝敗結果__ を表す __`status`__ という __属性が存在する__ ことを __知っている必要__ があります。もちろん、`Marubatsu` クラスを定義した作者はそのことは知っているはずですが、__作者以外__ の人が __`Marubatsu` クラス__ を __利用__ する場合は、__そのこと__ を何らかの方法で __学ぶ必要__ があります。そのため、__`play`__ メソッドの __返り値__ として、__勝敗結果__ を __返す__ ようにしたほうが __親切__ です。

他にも、話が長くなるので今回の記事では説明しませんが、__インスタンスの属性__ に対する __処理__（値の参照や代入）は、クラスの __メソッドの外__ では __なるべく行わないほうが良い__ という理由から、`mb.status` と記述して勝敗結果を参照するよりは、`play` メソッドの返り値として受け取ったほうが良いでしょう。

:::

## `play` メソッドの修正 2（途中経過の非表示）

先程のプログラムでは、__画面__ に AI どうしの __対戦__ の __途中経過__ が __表示__ されていましたが、AI の __強さを評価するため__ に __対戦__ を __複数回行う際__ に __重要__ となるのは、__勝敗結果だけ__ で、__途中経過__ の情報は __必要ではありません__。また、例えば __100 回の対戦__ を行った結果、__100 回分__ の対戦の __途中経過__ が画面に __表示__ されてしまうと、「__画面__ がその表示で __埋め尽くされてしまう__」、「__表示の処理__ に __時間がかかる__」などの __問題が発生__ します。ちなみに、__先程__ のプログラムで、__対戦回数__ を __2 回__ にした __理由__ は、__実行結果__ の __表示__ を __少なくするため__ でした。

__`play`__ メソッド __実行した際__ に、__対戦__ の __途中経過__ を __表示しない__ ように __修正__ すれば良いと __思う人がいるかもしれません__ が、__そのよう__ に __`play`__ メソッドを __修正__ してしまうと、今度は __人間と AI__ の対戦や、__人間どうし__ の __対戦__ の際に、__途中経過__ が __表示されなく__ なってしまい、__人間__ が __遊ぶことができなく__ なってしまうという __欠点__ が生じます。

そこで、__`play`__ メソッド __実行__ した際に、__途中経過__ を __表示するかどうか__ を __選択__ できるように __改良__ することにします。そのために、そのことを __選択するため__ の __仮引数__ を __追加__ します。

画面に __メッセージ__ を __表示するかどうか__ を __選択__ するための __仮引数__ の __名前__ として、画面に表示される __メッセージ__ が __デバッグのため__ のメッセージである場合は __`debug`__ が __良く使われます__。他にも「言葉数の多い」、「多弁な」という意味を表す __`verbose`__ という __名前__ も __良く使われる__ ようです。__途中経過__ の表示は __デバッグ__ のためのメッセージ __ではない__ ので、本記事では __仮引数の名前__ を __`verbose`__ とし、下記のプログラムのように、__`True`__ が代入されていた場合は __途中経過を表示__ し、__`False`__ が代入されていた場合は __途中経過を表示しない__ ように __`play`__ メソッドを __修正__ します。なお、__`play`__ メソッドで 〇×ゲームを __人間が遊ぶ際__ に、毎回 __実引数__ に __`verbose=True`__ を __記述__ するのは __面倒__ なので、__`verbose`__ は __デフォルト引数値__ を __`True`__ とする __デフォルト引数__ とします。

* __1 行目__：__デフォルト引数値__ が __`True`__ の __デフォルト引数 `verbose`__ を __追加__
* __7、12 行目__：__`verbose`__ が __`True`__ の __場合のみ__、__ゲームの画面を表示__ するように修正

なお、__人間が座標を入力__ する場合は、__メッセージを表示__ しなければ __ゲームを遊べない__ ので、__人間が座標を入力__ する __処理の中__ の __メッセージ__ は __常に表示__ するようにしています。

```python
 1  def play(self, ai, verbose=True):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.status == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          if verbose:
 8              print(self)
 9
元と同じなので省略
10
11      # 決着がついたので、ゲーム盤を表示する
12      if verbose:
13          print(self)
14      return self.status
15
16  Marubatsu.play = play
```

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

```python
def play(self, ai, verbose=True):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        if verbose:
            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("整数の座標を入力して下さい")

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

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

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

```diff_python
def play(self, ai, verbose=True):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
-       print(self)
+       if verbose:
+           print(self)

元と同じなので省略      

    # 決着がついたので、ゲーム盤を表示する
-   print(self)
+   if verbose:
+       print(self)
    return self.status

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


下記のプログラムのように、__実引数__ に __`verbose=False`__ を記述して `play` を実行することで、__途中経過__ が __表示されなくなった__ ことが確認できます。

```python
for _ in range(2):
    print("winner: ", mb.play(ai=[ai1, ai2], verbose=False))
```

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

```
winner:  o
winner:  o
```

## 勝敗結果の集計

__勝敗結果__ は、__〇 の勝利__、__× の勝利__、__引き分け__ の __3 種類__ があるので、下記のプログラムのように、__3 つの変数__ と、__if 文__ を使って __それぞれの回数__ を __集計__ することができます。

* __1 ~ 3 行目__：__〇 の勝利__、__× の勝利__、__引き分け__ の __回数を数える__ ための __`circle`、`cross`、`draw`__ という __3 つの変数__ に __`0` を代入__ して __初期化__ する
* __4 行目__：__for 文__ で __100 回__ の繰り返しを行う
* __5 行目__：__`ai1` と `ai2`__ の __対戦__ を行い、__結果__ を __`winner`__ に __代入__ する
* __6 ~ 11 行目__：__if 文__ を使って、__勝敗結果__ に __応じて__、__回数を数える変数__ に __`1` を足す__
* __13 行目__：__繰り返し処理__ が __終了した時点__ で __集計が完了__ するので、__結果を表示__ する

実行結果からわかるように、`ai1` VS `ai2` で 100 回対戦した結果は、`ai1` の 72 勝 18 敗 10 分 でした。なお、__`ai2`__ には __ランダム性がある__ ので、結果は下記と __異なる場合__ があります。

```python
 1  circle = 0
 2  cross = 0
 3  draw = 0
 4  for _ in range(100):
 5      winner = mb.play(ai=[ai1, ai2], verbose=False)
 6      if winner == Marubatsu.CIRCLE:
 7          circle += 1
 8      elif winner == Marubatsu.CROSS:
 9          cross += 1
10      else:
11          draw += 1
12
13  print("circle", circle, "cross", cross, "draw", draw)
```

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

```python
circle = 0
cross = 0
draw = 0
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    if winner == Marubatsu.CIRCLE:
        circle += 1
    elif winner == Marubatsu.CROSS:
        cross += 1
    else:
        draw += 1

print("circle", circle, "cross", cross, "draw", draw)
```
</details>

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

```
circle 72 cross 18 draw 10
```

### dict を使った集計

上記のプログラムでは、__3 つの変数__ と、__if 文__ を使って __集計__ を行いましたが、__dict__ を使うことで __集計__ をより __簡潔に記述__ することができます。

__勝敗結果__ を表す __`play`__ メソッドの __返り値__ の __データ型__ は __文字列型__ で、__dict__ は __キー__ として __文字列型__ のデータを __利用__ することが __できる__ ので、下記のプログラムのように、__`play`__ メソッドの __返り値__ を __dict のキー__ として __利用__ し、その __キーの値__ を使って __回数を数えます__。

* __1 ~ 5 行目__：__〇 の勝利__、__× の勝利__、__引き分け__ の回数を __数える__ ための、__`Marubatsu.CIRCLE`__、__`Marubatsu.CROSS`__、__`Marubatsu.EMPTY`__ を __キー__ として持ち、それぞれの __キーの値__ が __`0`__ である __dict__ を __`count`__ という __変数__ に __代入__する
* __6 行目__：__for 文__ で __100 回__ の __繰り返し__を行う
* __7 行目__：__`ai1` と `ai2`__ の __対戦__ を行い、__結果__ を __`winner`__ に __代入__ する
* __8 行目__：__`count`__ の __`winner`__ の __キーの値__ に __`1` を足して__、__勝敗結果__ に応じた __回数を数える__
* __10、11 行目__：__`count`__ のそれぞれの __キー__ と __その値__ を、[`items` メソッドと for 文による __繰り返し処理__](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) を使って __表示__ することで、__集計結果を表示__ する

実行結果から、`ai1` と `ai2` で 100 回対戦した結果は、`ai1` の 75 勝 21 負 4 分 であり、先程の結果と __大きく変わらない__ ことから正しく集計されていることが確認できます。

```python
 1  count = {
 2     Marubatsu.CIRCLE: 0,
 3     Marubatsu.CROSS: 0,
 4     Marubatsu.DRAW: 0,
 5  }
 6  for _ in range(100):
 7      winner = mb.play(ai=[ai1, ai2], verbose=False)
 8      count[winner] += 1
 9
10  for key, value in count.items():
11     print(key, value)
```

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

```python
count = {
    Marubatsu.CIRCLE: 0,
    Marubatsu.CROSS: 0,
    Marubatsu.DRAW: 0,
}
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    count[winner] += 1

for key, value in count.items():
    print(key, value)
```
</details>

実行結果（実行結果はランダムなので下記とは異なる場合があります）
```
o 75
x 21
draw 4
```

### defaultdict を使った集計

先程のプログラムでは、1 ~ 5 行目で __`0` が代入__ された __3 つのキー__ を持つ __dict__ を `count` に代入しましたが、その __記述が面倒__ だと思った人はいませんか？

先程のプログラムを、下記のように、__`count`__ に __空の dict__ を __代入__ するように __記述できれば__ プログラムが __簡潔__ になりますが、下記のプログラムを実行すると __エラーが発生__ します。

```python
count = {}
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    count[winner] += 1

for key, value in count.items():
    print(key, value)
```

実行結果

```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[9], line 4
      2 for _ in range(100):
      3     winner = mb.play(ai=[ai1, ai2], verbose=False)
----> 4     count[winner] += 1
      6 for key, value in count.items():
      7     print(key, value)

KeyError: 'o'
```

上記のような __エラーが発生__ する __理由__ は、下記のプログラムのように、__値が一度も代入されていない dict__  の __キーを参照__ しようとすると __KeyError__ という __エラーが発生__ するからです。

```python
count = {}
print(count[Marubatsu.CIRCLE])
```
実行結果
```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[10], line 2
      1 count = {}
----> 2 print(count[Marubatsu.CIRCLE])

KeyError: 'o'
```

下記のプログラムの 4 行目の __`count[winner] += 1`__ という式は、__`count[winner] = count[winner] + 1`__ という処理を行う式です。1 行目の処理によって、__`count`__ に __空の dict__  が __代入された状態__ で __この式を実行__ しようとすると、代入文の `=` の右の __`count[winner] + 1`__ を __計算する際__ に、__値が一度も代入されていない__ dict の __キーの値を参照__ する必要があるため、このプログラムを実行すると __KeyError__ という __エラーが発生__ します。

```python
count = {}
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    count[winner] += 1
```

#### collection モジュールと defaultdict

この問題は、Python の __collections__ という __モジュール__ の中で定義された、__defaultdict__ という __クラス__ を利用することで __解決__ することができます。

Python では、list、tuple、dict などの、__複数のデータを扱う__ ことができる __データ型__ の事を __コンテナデータ型__ と呼びます。__collections__ は、Python が __標準的に用意__ する、list、tuple、dict、set  __以外__ の __特殊なコンテナデータ型__ が __定義__ された __モジュール__ です。

:::note
[以前の記事](https://qiita.com/ysgeso/items/9efd60c7a28381e6cbf7#%E8%A4%87%E5%90%88%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B%E3%81%AE%E5%AE%9A%E7%BE%A9) で、__複合データ型__ という用語を下記のように定義しました。

Python の __任意のデータ型__ を、__複数組み合わせて__ データを表現するデータ型

この定義と __コンテナデータ型__ は __似ているように見える__ かもしれませんが、__微妙に異なり__ ます。その理由は、__文字のみ__ しか __格納__ できない __文字列型__ のデータが、__コンテナ型に分類__ されるからです[^2]。実際に、collections モジュールの中に、__文字列に関する UserString__ という __コンテナデータ型__ のクラスが __存在します__。このことが、以前の記事で、わざわざ __複合データ型__ という、__独自の用語__ を __定義した理由__ です。
:::

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

https://docs.python.org/ja/3/library/collections.html?highlight=defaultdict

#### defaultdict の使い方

__defaultdict__ は、「__一度も値が代入されていないキーを参照__ しようとした場合に、__エラーとせず__ に、__特定の値__ をその __キーの値に代入__ する」__以外__ では、__dict と同じ性質__ を持ちます[^3]。本記事では、__その特定の値__ の事を、defaultdict の __規定値__ と表記することにします。

__defaultdict__ は __クラス__ で、以下のように記述することで __インスタンスを作成__ します。__作成された__ の defaultdict の __インスタンス__ には、__空の dict__ と同様に、__キー__ は __存在しません__。

```python
defaultdict(default_factory)
```

実引数 __`default_factory`__ には、defaultdict の __規定値__ を __作成するため__ に __呼び出す__、__関数__ または __クラス__ を __記述__ します。例えば、__既定値__ として、__整数型__ の __`0`__ を __代入__ したい場合は、下記のプログラムのように __`default_factory`__ に __`int` を記述__ します。

:::note
__`int`__ を __記述__ すると __既定値__ が __`0` になる理由__ については __後述__ します。また、実引数に、__既定値である `0`__ を __直接記述しない理由__ についても __後述__ します。
:::

* __1 行目__：__`collections`__ モジュールから __`defaultdict` をインポート__ する
* __3 行目__：__`defaultdict`__ の __実引数__ に `int` を記述することで、__一度も値が代入されていないキー__ を __参照__ しようとした場合に、__既定値__ として __`0`__  がその __キーの値__ に __代入__ される

__4 行目__ を実行した時点で __`count`__ には __`Marubatsu.CIRCLE`__ という __キーは存在しない__ ので、__`count[Marubatsu.CIRCLE]`__ に __`0` が代入__ され、その結果 __`0` が表示__ されます。

```python
from collections import defaultdict

count = defaultdict(int)
print(count[Marubatsu.CIRCLE])
```

実行結果

```
0
```

先程のプログラムは、下記のプログラムのように __defaultdict__ で __簡潔に記述__ できます。

```python
count = defaultdict(int)
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    count[winner] += 1

for key, value in count.items():
    print(key, value)    
```

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

```diff_python
-count = {
-    Marubatsu.CIRCLE: 0,
-    Marubatsu.CROSS: 0,
-    Marubatsu.DRAW: 0,
-}
+count = defaultdict(int)
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    count[winner] += 1

for key, value in count.items():
    print(key, value)
```
</details>

実行結果（実行結果はランダムなので下記とは異なる場合があります）
```
x 20
o 78
draw 2
```

### defaultdict に関する補足

__for 文__ に記述された __`count.items()`__ は、__defaultdict__ に __登録された順__ で __キー__ と __その値__ を __取り出す__ ので、例えば __最初__ の __`mb.play()`__ の __返り値__ が __`Marubatsu.CROSS`__ だった場合は、上記の実行結果のように、__最初__ に __`x` の勝利の回数__ が __表示__ される __場合__ があります。

また、__一度も引き分けにならなかった__ 場合は、__`Marubatsu.DRAW`__ という __キー__ が __`count`__ に __登録されない__ ので、下記のように __`draw`__ が __実行結果__ に __表示されない場合__ もあります。

```
o 80
x 20
```

ただし、キーの __登録の順番__ が __処理に影響を与える__ のは、`items` メソッドなどで __キー__ や __その値__ を __順番に取り出す場合だけ__ なので、それ以外の場合は __気にする必要はない__ でしょう。

### defaultdict が行う処理

__`defaultdict(default_factory)`__ の __`default_factory`__ に __`int`__ を記述すると、__既定値__ として __`0` が設定__ される __理由__ について説明します。__defaultdict__ に対して、__一度も値が代入されていないキー__ を __参照__ しようとした場合は、__下記の手順__ で __処理__ が行われます。

1. __`default_factory`__ が __呼び出される__。具体的には __`default_factory()` が実行__ される
2. __`default_factory()`__ の __返り値__ が、その __キーの値__ として __代入__ される

:::note warn
`default_factory` の __データの種類__ によって、__規定値__ は以下のようになります。
* __クラス__ を記述した場合は、その __クラスのインスタンス__ が __既定値__ となる
* __関数__ を記述した場合は、その __関数__ の __返り値__ が __規定値__ となる
:::

:::note
__`default_factory`__ は、__defaultdict__ の __`__init__`__ メソッドの __仮引数の名前__ で、__由来__ は __既定（default）値__ を __作成__ する __工場__ （factory）のような __役割__ を持つことです。
:::

__`default_factory`__ に __`int`__ を __記述__ した場合、__`int()`__ という __処理が実行__ されます。__`int`__ は __[組み込み型のクラス](https://qiita.com/ysgeso/items/a189461ae32a6531ff7f#%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF%E5%9E%8B%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%B9)__ で、__[`int()`](https://docs.python.org/ja/3/library/functions.html#int)__ のように、__実引数__ に __何も記述せず__ に __呼び出した場合__ は、下記のプログラムのように、__整数型__ の __`0`__ を表す __インスタンスが作成__ されます。

これが、__`defaultdict(int)`__ によって、__既定値__ が __`0`__ の  __defaultdict__ が __作成される理由__ です。

```python
print(int())
```

__defaultdict__ では、__既定値__ として __空の list__ を __指定__ することが __良くあります__。その場合は、下記のプログラムのように、__`default_factory`__ に __`list` を記述__ します。これは、__`list`__ が __組み込み型のクラス__ で、[__`list()`__](https://docs.python.org/ja/3/library/stdtypes.html#list) のように、__実引数__ に __何も記述せず__ に __呼び出した場合__ は、__空の list__ を表す __インスタンスが作成__ されるからです。

```python
count = defaultdict(list)
print(count[Marubatsu.CIRCLE])
```

実行結果

```
[]
```

:::note warn
__defaultdict__ の __規定値__ に __整数型の `0`__ を __指定__ したい場合は __`defaultdict(int)`__ を、__空の list__ を __指定__ したい場合は __`defaultdict(list)`__ と記述すれば良い。
:::

### 任意の値を規定値とする defaultdict

今回の記事では利用しませんが、`0` や、空の list 以外の __任意の値__ を __規定値__ とする  __defaultdict__ を __作成__ することが __できます__。その場合は、__規定値__ として __設定したい値__ を __返す__、__仮引数__ が __存在しない関数__ を __定義__ し、__その関数__ を __`default_factory`__ に __記述__ します。

例えば、下記は、__整数型__ の __`1`__ を __規定値__ とする __defaultdict__ を __作成__ するプログラムです。5 行目の実行結果から、__既定値__ として __`1` が代入__ されることが __確認__ できます。

* __1、2 行目__：常に整数型の __`1`__ を返す `create_one` という名前の関数を定義する
* __4 行目__：__`create_one`__ を __`defaultdict`__ の __実引数__ に __記述__ して呼び出すことで、__規定値__ が __`1`__ の __defaultdict__ を __作成__ して、__`count` に代入__ する

```python
def create_one():
    return 1

count = defaultdict(create_one)
print(count[Marubatsu.CIRCLE])
```
実行結果
```
1
```

### defaultdict の実引数に、規定値を直接記述しない理由

__既定値__ を __`1`__ とする __defaultdict__ を __作成__ する際に、下記のプログラムのように、defaultdict の __実引数__ に __既定値__ を __直接記述したい__ と思う人がいるかもしれませんが、下記のプログラムを実行すると __エラーが発生__ します。その __理由__ は、__defaultdict__ の `default_factory` に対応する __実引数__ は、__関数__ や __クラス__ などの、__呼び出すことができる__ オブジェクト __でなければならない__ からで、__整数型__ の __`1`__ は __`1()`__ のように記述して __呼び出すことはできない__ からです。

```python
count = defaultdict(1)
print(count[Marubatsu.count])
```

実行結果
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[16], line 1
----> 1 count = defaultdict(1)
      2 print(count[Marubatsu.count])

TypeError: first argument must be callable or None
```

:::note
上記のエラーメッセージは、以下のような意味を持ちます。

* __TypeError__
データ型（type）に関するエラー
* __first argument must be callable or None__
（defaultdict の）最初（first）の実引数（argument）は、（関数やクラスなどの）呼び出し可能（callable）なオブジェクトか、`None` でなければならない（must be）
:::

__defaultdict__ の __実引数__ に、__規定値__ を __直接記述しない__ 理由は、そのようにすると、__作成した__ defaultdict の __規定値__ が、__すべてのキーで常に同じ値__ になってしまうからです。例えば、__既定値__ を __直接記述__ するという __方法__ では、defaultdict の __規定値__ として、__値が代入されていないキー__ の値を __参照した時刻__ を __設定__ することは __できません__。

一方、__defaultdict__ の __実引数__ に、__既定値__ を __作成する__ ための __関数やクラス__ を __記述__ する場合は、下記のプログラムのように、__現在時刻__ を __返す関数__ を __定義__ し、__その関数__ を defaultdict の __実引数に記述__ することで、__現在時刻__ を表す __文字列__ を __既定値__ として持つ __defaultdict を作成__ することが __できます__。なお、__現在時刻__ を表す __文字列__ は、__time__ という __モジュール__ で定義された __`ctime`__ という __関数__ を呼び出すことで得ることができます。

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

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

https://docs.python.org/ja/3/library/time.html?highlight=time#time.ctime

* __1 行目__：__time__ モジュールから、__`ctime`__ を __インポート__ する
* __3、4 行目__：__現在の時刻__ を表す __文字列__ を __返す__、__`now`__ という関数を __定義__ する
* __6 行目__：__`now`__ を __実引数__ に記述して、__defaultdict を作成__ し、__`count`__ に __代入__ する

下記のプログラムを実行すると、__`count`__ の __規定値__ が、__値が代入されていないキー__ の値を __参照__ した時の __時刻__ を表す __文字列__ になるので、7 行目を実行すると、__7 行目__ を __実行した時__ の __時刻を表す文字列__ が __表示__ されます。

```python
1  from time import ctime
2
3  def now():
4      return ctime()
5
6  count = defaultdict(now)
7  print(count(Marubatsu.CIRCLE))
```

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

```python
from time import ctime

def now():
    return ctime()

count = defaultdict(now)
print(count[Marubatsu.CIRCLE])
```
</details>

実行結果
```
Fri Dec 29 11:39:50 2023
```

また、上記のプログラムを __実行した少し後__ で、下記のプログラムを実行すると、__異なるキー__ に対して、__異なる規定値__ が __代入__ されることが __確認__ できます。

:::note
__`ctime`__ は __秒単位__ で __時刻を表す__ するので、上記のプログラムと下記のプログラムを __間を空けず__ に __連続して実行__ すると、__同じ文字列が表示__ されます。
:::

```python
print(count[Marubatsu.CROSS])
```
</details>

実行結果
```
Fri Dec 29 11:40:01 2023
```

:::note warn
__defaultdict__ の __実引数__ に __既定値__ を __直接記述しない理由__ は、__実引数__ に __関数やクラス__ を __記述したほう__ が、__状況に応じて値が変化する__ ような、__柔軟な既定値__ を __設定__ することが __できるようになる__ ためです。
:::

:::note
上記のプログラムでは、__`ctime()`__ を __返り値__ として __返す__、__`now`__ という __関数を定義__ しましたが、__`ctime()`__ は、__`ctime`__ を __呼び出した時__ の __返り値__ なので、下記のプログラムのように、__`ctime`__ を __直接__  defaultdict の __実引数に記述__ することが __できます__。

```python
count = defaultdict(ctime)
print(count[Marubatsu.CIRCLE])
```
実行結果
```
Fri Dec 29 11:40:09 2023
```

:::

### ラムダ式

今回の記事では __defaultdict__ の __規定値__ に __`0` を設定__ するため、__`defaultdict(int)`__ とだけ記述すれば良いので、下記の説明の __ラムダ式__ を利用することはありませんが、__ラムダ式__ は __実際に良く使われる__ ので説明することにします。

__今回の記事__ のプログラムでは、__ラムダ式を使わない__ ので、__分かりづらい__ と思った方は、__そういうものがある__ ということだけを __覚えて__ おいて、__読み飛ばして__ も __かまいません__。

:::note warn
__ラムダ式__ は __うまく使えば便利__ ですが、むやみに使うとプログラムが __わかりづらくなる__ という __欠点__ があります。そのため、__慣れないうち__ は、__ラムダ式__ を使うと __便利な例__ として学んだ場合で __のみで使う__ ことを __強くお勧めします__。
:::

`0` や 空の list __以外__ の __既定値__ を持つ __defaultdict__ を __利用__ する際に、上記のような __規定値を返す関数__ を __定義__ するのは __面倒__ なので、__defaultdict__ の __default_factory__ は、より __簡潔に記述__ できる __ラムダ（lambda）式__ を使って __記述する__ のが __一般的__ です。

__ラムダ式__ は、__無名関数__ という、__名前の無い関数__ を __簡潔に定義__ することができる __式__ で、下記のように記述します。__仮引数__ が __存在しない場合__ は、__`lambda : 式`__ のように記述します。

```python
lambda 仮引数: 式
```

__上記のラムダ式__ は、__下記__ の __関数の定義__ と __同じ意味__ を持ちます。なお、先ほど説明したように、__ラムダ式__ は __名前の無い関数__ を __定義__ するので、それに __あわせて__ 下記のプログラムでは __関数の名前__ を __記述していません__ が、__def__ による __関数の定義__ で __名前を記述しない__ と __エラーが発生する__ ので、__実際に__ 下記のプログラムを記述して __実行__ すると __エラーが発生__ します。
下記のプログラムは、__ラムダ式の説明__ を行うための __架空の記述__ だと思ってください。

```python
def (仮引数):
    return 式
```

上記のような、__def__ で __名前の無い関数__ を __定義__ することが __できない理由__ は、__名前の無い関数__ を __定義しても__、__名前が存在しないため__、その __関数を利用することができない__ からです。

ラムダ式の詳細については下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/reference/expressions.html#lambda

#### ラムダ式の利用方法

__ラムダ式__ は __関数の定義__ なので、__ラムダ式を実行__ しても __def__ による __関数の定義__ と __同様__ に、__関数のブロックの処理__ は __行われません__。また、__ラムダ式__ で定義した __関数__ には __名前がない__ ので、__ラムダ式__ を __記述しただけ__ では、その __関数__ を __利用__ することは __できません__。

そのため、ラムダ式は、以下の __いずれかの方法__ で __利用__ します。

* __関数呼び出し__ の __実引数__ に __記述__ する。この場合は、__ラムダ式__ で __定義された関数__ が、関数の __仮引数に代入__ されるので、__仮引数__ を使って __利用__ することができる
* __変数__ やインスタンスの __属性__ に __代入__ し、代入した __変数__ や __属性__ を使って __利用__ する

後で具体的に説明しますが、__defaultdict__ で利用する __ラムダ式__ は、__前者__ の利用方法です。

下記のプログラムは、__後者の利用方法__ の __具体例__ ですが、実際には、このような、プログラムの __複数の場所__ で __関数の名前__ を __直接記述__ して __何度も呼び出す__ ような場合は __ラムダ式__ は一般的には __使われません__。その __理由の一つ__ は、__ラムダ式__ で記述した関数は、__def__ で定義した場合と __比べて__、__定義した場所__ と、__見た目__ が __わかりづらくなる__ ためです。

__後者__ の __有効な利用方法__ については、必要になった時点で説明します。

```python
add = lambda x, y: x + y
print(add(1, 2))
print(add(2, 3))
```
実行結果
```
3
5
```

:::note
__`ai2`__ は、__ブロック内__ で行う __処理__ を、__1 つの return 文__ で __記述__ することが __できる__ ので、__ラムダ式__ を使って、下記のプログラムのように __定義__ することができます。その下の元のプログラムと見比べてみて下さい。

```python
from random import choice

ai2 = lambda mb: choice(mb.calc_legal_moves())
```
元のプログラム
```python
def ai2(mb):
    legal_moves = mb.calc_legal_moves()
    return choice(legal_moves)
```

下記のプログラムを実行することで、`ai1` と上記の `ai2` で対戦を行うことができることが確認できます。なお、実行結果は長くなるので省略します。

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

__1 つの return 文__ で __記述できない__ ような __AI__ は __ラムダ式__ では __記述できません__。また、上記のラムダ式は __わかりづらい__ ので、__`ai2`__ は __元のプログラムを採用__ します。

:::


#### ラムダ式の制限事項

__ラムダ式__ は、関数を簡潔に定義できますが、以下のような __制限があります__。

* __関数__ が行う __処理__ として、__複数の文__ を __記述__ することは __できない__。例えば、下記のような、__複数の文__ から __構成される関数__ を __ラムダ式__ で __記述__ することは __できない__

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

:::note warn
ラムダ式は、__1 つの式__ の __計算結果を返す__ ような __関数しか定義できない__。
:::

* 仮引数や返り値に対する __型アノテーションを記述__ することが __できない__。例えば、下記のようなプログラムを記述するとエラーが発生する

```python
lambda a:int : a -> int
```

:::note warn
1 つ目の __制限事項__ から、__ほとんどの関数__ は __ラムダ式__ で __記述__ することは __できません__ が、__defaultdict__ など、__一部の状況__ では __ラムダ式__ を使うことでプログラムを __簡潔に記述できる__ ので、そのような __特別な場合__ で __ラムダ式__ は __使われます__。
:::

### ラムダ式を利用した defaultdict

__ラムダ式__ を __利用__ することで、先程の、__規定値__ が __整数型の `1`__ となる __defaultdict を作成__ するプログラムを、下記のプログラムのように __簡潔に記述__ できます。__修正箇所__ を見て、__ラムダ式を使わない__ プログラムと __見比べて__ みて下さい。


```python
count = defaultdict(lambda : 1)
print(count[Marubatsu.CIRCLE])
```
実行結果
```
1
```

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

```diff_python
-def create_one():
-   return 1
-count = defaultdict(create_one)
+count = defaultdict(lambda : 1)
print(count[Marubatsu.CIRCLE])
```
</details>

:::note
__既定値__ が __`0`__ の defaultdict は __`defaultdict(int)`__ のように __簡潔に記述できる__ ので、わざわざ __`defaultdict(lambda : 0)`__ のように __記述__ する __必要はありません__。
:::

__ラムダ式__ は、__defaultdict 以外__ の __用途__ でも __使われます__。ラムダ式の他の具体的な利用方法については、必要になった時点で紹介します。

## AI どうしの対戦を行う関数の定義

__AI どうしの対戦__ を行う際に、先程のようなプログラムを __毎回記述__ するのは __大変__ なので、__AI どうしの対戦__ を行う __関数を定義__ する事にします。その際に、__対戦__ に __時間がかかる可能性__ があることを __考慮__ して、__対戦回数__ を __指定できる__ ようにします。

__処理__：__AI どうし__ の __複数回__ の __対戦__ を行い、__通算成績を表示__ する

__名前__：AI どうしの __対戦__（match）を行うので、__`ai_match`__ という名前にする

__入力__：
* __`ai`__ という __仮引数__ に __AI の処理__ を行う __関数__ を、__`play`__ メソッドの仮引数 __`ai`__ と __同じデータ構造__ で __代入__ する
* __`match_num`__ という __仮引数__ に __対戦__ を __行う回数__ を __代入__ する。また、__`10000`__ を __デフォルト値__ とする __デフォルト引数__ とする

__出力__：なし（__通算成績__ は __文字で表示__ する）

下記は、__`ai_match` の定義__ です。__基本的__ には、__先程のプログラム__ で行っていた __処理__ を __`ai_match`__ の関数の __ブロックに記述__ していますが、__下記の点__ が __異なります__。

* __1 行目__：__名前__ を __`ai_match`__、__仮引数__ を __`ai`__ と __`match_num`__ とし、__`match_num`__ を __デフォルト値__ が __`10000`__ の __デフォルト引数__ とする __関数を定義__ する
* __4 行目__：__for 文__ で、__`match_num` 回__ の __繰り返し処理__ を行う
* __7、8 行目__：先ほど説明したように、__defaultdict__ を使った場合に __for 文__ と __`count.items()`__ を利用して __キー__ と __その値__ を __表示__ すると、__表示の順番__ が 〇 の勝利、× の勝利、引き分けの __順にならない場合がある__ ので、__必ずその順番__ で __表示する__ ように __修正__ した

```python
1  def ai_match(ai, match_num=10000):
2      mb = Marubatsu()
3      count = defaultdict(int)
4      for _ in range(match_num):
5          count[mb.play(ai, verbose=False)] += 1
6
7      print("o", count[Marubatsu.CIRCLE], "x", count[Marubatsu.CROSS],
8            "draw", count[Marubatsu.DRAW])
```

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

```python
def ai_match(ai, match_num=10000):
    mb = Marubatsu()
    count = defaultdict(int)
    for _ in range(match_num):
        count[mb.play(ai, verbose=False)] += 1

    print("o", count[Marubatsu.CIRCLE], "x", count[Marubatsu.CROSS],
          "draw", count[Marubatsu.DRAW])
```
</details>

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

関数のブロックのインデントは修正箇所に含めません。

```diff_python
+def ai_match(ai, match_num=10000):
    mb = Marubatsu()
    count = defaultdict(int)
-   for _ in range(100):
+   for _ in range(match_num):
        count[mb.play(ai, verbose=False)] += 1

-for key, value in count.items():
-   print(key, value)   
+   print("o", count[Marubatsu.CIRCLE], "x", count[Marubatsu.CROSS],
          "draw", count[Marubatsu.DRAW])
```
</details>


下記のプログラムを実行することで、__`ai1` VS `ai2`__ の __対戦__ を 1 行目で __1000 回__、2 行目で __10000 回__ 行い、__通算成績を表示__ することができることが __確認__ できます。

```python
ai_match(ai=[ai1, ai2], match_num=1000)
ai_match(ai=[ai1, ai2])
```

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

```
o 759 x 207 draw 34
o 7834 x 1733 draw 433
```

## `ai1` と `ai2` のすべての組み合わせの対戦とその評価

対戦を行うための関数が定義できたので、下記のプログラムで __`ai1` と `ai2`__ の __すべての組み合わせ__ で __対戦を行い__、__それぞれ__ の __通算成績を評価__ することにします。

```python
ai_match(ai=[ai1, ai1]) # ai1 VS ai1
ai_match(ai=[ai1, ai2]) # ai1 VS ai2
ai_match(ai=[ai2, ai1]) # ai2 VS ai1
ai_match(ai=[ai2, ai2]) # ai2 VS ai2
```

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

```
o 10000 x 0 draw 0
o 7727 x 1816 draw 457
o 5208 x 4392 draw 400
o 5851 x 2878 draw 1271
```

実行結果を __ざっと眺める__ ことで、下記の事がわかります。

* __`ai1` VS `ai1`__ では、__`ai1`__ の __アルゴリズム__ には __ランダム性はない__ ので、10000 回の __すべての対戦__ で __〇 が勝利__ する
* __`ai1` VS `ai2`__ と __`ai1` VS `ai2`__ の __通算成績__ を __合計__ すると、__`ai1` から見て__ $7727 + 4392 =$ __12119 勝__、$1816 + 5208 =$ __7024 敗__、$457 + 400 =$ __857 分__ であり、__`ai1` のほうが `ai2` より強く見える__
* __`ai2` VS `ai2`__ から、__`ai2` どうし__ で対戦した場合は、__〇 のほうが__ × の __約 2 倍勝利__ する

__ランダムな着手__ を行う __`ai2` どうし__ の __対戦結果__ から、〇×ゲームは __〇 のほうが__ かなり __有利__ なゲームであると __推測__ することができます。

### `ai1` と `ai2` の強さ考察

上記の結果から、__一見する__ と、__`ai1` のほうが `ai2` より強い__ ように __思えるかも__ しれませんが、そのような __結論を出す__ のは __早計__ です。それぞれの AI の __実際の強さとは別に__、__`ai1` のアルゴリズム__ が __`ai2` のアルゴリズム__ に対して __相性が良い可能性__ があるからです。

そこで、上記の __実行結果__ を __考慮せず__ に、`ai1` と `ai2` の __アルゴリズムだけ__ から、__どちらが強い__ AI であるかを __考察__ してみることにします。

〇×ゲームのことを __あまりよく知らない人間__ と、__`ai1` が対戦する場合__ のことを考えてみて下さい。__`ai1`__ は __左上のマスから順番__ に __空いているマスを探して着手を行う__ だけなので、__人間__ であれば、初心者であっても __すぐに `ai1` に負けなくなる__ はずです。

一方、__`ai2`__ は __ランダムな着手__ を行うので、__偶然__ `ai2` が __最善手のみの着手__ を行う __可能性__ があり、そのような場合は 〇×ゲームの __初心者が負ける可能性__ はあるでしょう。

上記から、__人間相手__ の __対戦__ を考えると、`ai1` より __`ai2` のほうが強い__ と __考察__ できます。

しかし、実際に __`ai1` と `ai2` で対戦__ した場合は、__`ai1`__ が __大きく勝ち越し__ ます。そのような結果になる理由について少し考えてみて下さい。

### `ai1` が `ai2` に勝ち越す理由

__`ai1`__ が __〇 を担当__ する場合は、その __アルゴリズムから__、__`ai2`__ が __上の行__ に __着手を一度も行わなければ__、__3 回の着手__ で __`ai1`__ は __上の行__ に __〇 を順番に着手__ して __勝利__ します。__`ai2`__ は __ランダムな着手__ を行うので、__2 手連続__ で __上の行__ に __着手を行わない確率__ は計算しなくても __かなりありそう__ です。従って、__`ai1`__ が __〇 を担当__ する場合に、__`ai2` に勝つ確率__ はかなりあると __推測__ できます。__`ai1`__ が __× を担当__ する場合は __`ai2`__ が __3 手連続__ で __上の行__ に __着手を行わなければ__、同様の理由で __`ai1` が勝利__ しますが、その __確率__ もそれほど __低くはなさそう__ です。従って、__`ai1`__ は __`ai2`__ に対して __相性が良い__ アルゴリズムであることが __推測__ できます。

ただし、上記は __推測に過ぎない__ ので、実際にそうであるかどうかは、__確率を計算__ するなどの方法で __検証する必要__ があります。そこで、__`ai1`__ が __〇 を担当__ する場合に、`ai2` が __2 手連続__ で __上の行__ に __着手を行わない__ 確率の求めることにします。

#### __`ai1`__ が __〇 を担当__ する場合に、`ai2` が __2 手連続__ で __上の行__ に __着手を行わない__ 確率

__`ai2`__ が __1、2 手目__ に __一行目に着手しない__ ことによって、__`ai1` が勝利__ する __確率__ は、__下記の手順__ で __求める__ ことができます。

* __`ai2`__ の __初手__ で __空いているマスの数__ は __8 マス__、__2 手目__ では __6 マス__ なので、__1, 2 手目__ で __`ai2`__ が __着手__ を行うことができる __マスの組み合わせ__ は $8 * 6 =$ __48 通り__
* __`ai1`__ は __初手__ で __必ず (0, 0)__ のマスに __着手する__ ので、__`ai2`__ の __初手__ で __1 行目以外__ で __空いているマス__ は __6 マス__ である
* __`ai2`__ が __初手__ で __一番上の行__ に __着手しない__ 場合に __`ai2`__ は __2 手目__ で __必ず (1, 0)__ のマスに __着手__ するので、__`ai2`__ の __2 手目__ で __1 行目以外__ で __空いているマス__ は __5 マス__ である
* 従って、__`ai2`__ が __初手と 2 手目__ で __1 行目以外__ に __着手__ を行うことができる __マスの組み合わせ__ は $6 * 5 = $ __30 通り__
* 従って、__`ai2`__ が __初手と 2 手目__ で __1 行目以外__ に __着手__ を行って __`ai1` が勝利__ する __確率__ は、$30 / 48 = $ __62.5 %__ である

__`ai1`__ は __上記以外__ でも __勝利する場合がある__ ので、__実際の勝率__ は __62.5 % 以上__ になります。

:::note
下記の __考察__ は、__数学的__ には __厳密でない__ 考察ですが、__`ai1`__ という AI は特に __重要な AI__ では __ありません__ ので、`ai1` が `ai2` に対して相性が良いということを、__厳密に証明__ する __必要はない__ と思います。従って、下記のような雑な考察で __十分__ でしょう。
:::

この結果から、__〇 を `ai1` が担当__ した場合は、先程の、__`ai1`__ は __`ai2`__ に対して __相性が良い__ アルゴリズムであるという __推測__ が __正しい__ ことが __裏付けられ__ ました。__本当は__、__他の場合__ で `ai1` が勝利する確率や、__`ai1` が × を担当__ した場合の勝利の確率などを __計算したほうが良い__ のですが、先ほどの、__人間相手__ であれば __`ai2` のほうが__ `ai1` よりも __強い__ という __考察__ と、__`ai1` と `ai2` の対戦__ では __`ai1` のほうが__ `ai2` よりも __強い__ という __結果__ から、__`ai1`__ の __アルゴリズム__ は、__`ai2`__ の __アルゴリズム__ に対して __相性が良い__ と __結論付けても問題はない__ でしょう。

:::note
本記事では計算しませんが、興味と余裕がある方は、__他の場合__ で `ai1` が __勝利する確率__ や、__`ai1` が × を担当__ した場合の __勝利の確率__ などを計算してみて下さい。
:::

今回の __記事の最初__ で、じゃんけんの AI の例を使って「__基準__ となる AI が __偏った性質を持つ__ 場合に、__正しい評価__ が __行えない__」ことを説明しましたが、〇×ゲームの AI の場合でも、`ai1` と `ai2` の例で __そのことが説明__ できます。__`ai1`__ を __基準となる AI__  に __しなかった__ のは、ランダム性がないだけではなく、__偏った性質__ を持つ AI だからです。

# 今回の記事のまとめ

今回の記事では、以下のような、__AI の強さの評価__ を行う __方法__ を決め、その評価を行うための __`ai_match`__ という __関数を定義__ しました。ただし、この関数は本格的な AI の評価を行うためには __いくつかの問題__ があるので、次回の記事では __`ai_match` を改良__ することにします。

* __対戦の回数__
    * AI どうしの __対戦の回数__ は、__原則__ として __一万回__ 行う
    * 一万回の対戦に __時間がかかりすぎる場合__ は、__1 分で行える回数__ の対戦を行う
* __対戦の方法__
__新しい AI を作成__ した場合は、下記の AI と対戦を行う
    * ランダムな着手を行う __`ai2`__
    * __それまでに作成__ した AI の中で __最も強い AI__ と思われるうちの __いくつか__

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

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

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

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

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

以下のリンクは、今回の記事で更新した ai.py です。なお、`ai_match` はこちらに保存しました。

https://github.com/ysgeso/marubatsu/blob/master/040/ai_new.py

# 次回の記事

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

[^1]:[前回の記事](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#%E6%94%B9%E8%89%AF%E3%81%97%E3%81%9F-play-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%8B%95%E4%BD%9C%E3%81%AE%E7%A2%BA%E8%AA%8D)で説明したように、`ai1` VS `ai2` では、先に記述した `ai1` が 〇を担当します
[^2]:Python の公式ドキュメントの __[文字列型の説明](https://docs.python.org/ja/3/library/stdtypes.html#text-sequence-type-str)__ の所には、文字列型がコンテナ型であると明記されていませんが、他の文章でコンテナの説明に文字列型を含む説明を行っている例が実際に存在する点と、collections の公式ドキュメントの中に、UserString が含まれていることから、本記事では文字列型のデータをコンテナデータ型に含めることにします
[^3]:defaultdict という名前は、dict が持つ性質に加え、値が代入されていないキーの規定（default）値を設定することができるという性質を持つことが由来です