Pythonで〇×ゲームのAIを一から作成する　その42　手番を入れ替えた対戦

# 目次と前回の記事

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

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

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

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

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

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

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

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

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

# `ai_match` の改良 その 4（手番を入れ替えた対戦）

前回の記事では `ai_match` のいくつかの改良を行いました。今回の記事では、引き続き `ai_match` の改良を行います。

〇× ゲームを遊んでみるとすぐにわかると思いますが、__〇× ゲーム__ は、__〇 の手番__ と __× の手番__ で __有利不利__ が __大きく変わる__ ゲームです。そのため、__AI どうし__ の __強さを測る__ ためには、__〇 と × の担当__ を __入れ替え__ た対戦も __行う必要__ がありますが、__`ai_match`__ は __片方__ の対戦 __しか行いません__。例えば __`ai1` と `ai2`__ の場合は、__`ai1` VS `ai2`__ と __`ai2` VS `ai1`__ の __両方の対戦__ を行う __必要__ があります。そこで、__実引数で指定__ した __2 つの AI__ を、__〇 と ×__ の __手番を入れ替え__ た状態でも __対戦を行う__ ように `ai_match` を __修正__ することにします。

この修正では、`ai_match` の修正箇所が多いので、順番に少しづつ修正することにします。

## `count` の修正

手番を入れ替えない場合と、入れ替えた場合の __2 種類__ の __対戦__ を行うので、__2 種類__ の __通算成績__ を __数える__ 必要があります。__元のプログラム__ では、__`count`__ という __変数__ で __通算成績__ を数えていましたが、__手番を入れ替えた場合__ の通算成績を __`count2`__ のような __別の変数__ に代入すると __プログラムの記述__ が __面倒__ なことになるので、下記のプログラムの 5 行目のように、__`count_list`__ という変数に、__手番を入れ替えない__ 場合と __手番を入れ替える場合__ の __通算成績__ を __数える__ ための __defaultdict__ を __要素__ として持つ __list を代入__ することにします。

```python
1  def ai_match(ai, match_num=10000):
2      mb = Marubatsu()
3
4      # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
5      count_list = [ defaultdict(int), defaultdict(int) ]
元と同じなので省略
```

:::note
上記を __`count_list = [defaultdict(int)] * 2`__ のように記述すると、[以前の記事](https://qiita.com/ysgeso/items/59e51d983a32ac4bc5db#---3--3-%E3%81%A7%E8%A1%8C%E3%82%8F%E3%82%8C%E3%82%8B%E5%87%A6%E7%90%86)で説明したように、__`count_list`__ の __0 番の要素__ と、__1 番の要素__ が、__同一の defaultdict__ 表すことになるため、プログラムが __正しく動作しなくなる__ 点に注意して下さい。
:::

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

```diff_python
def ai_match(ai, match_num=10000):
    mb = Marubatsu()

    # # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
-   count = defaultdict(int)
+   count_list = [ defaultdict(int), defaultdict(int) ]
元と同じなので省略
```
</details>

なお、上記の __修正途中__ のプログラムを __実行__ すると __エラーが発生__ するので、__行番号のないプログラム__ は __省略__ します。また、プログラムが __正しく動作するか__ どうかの __確認__ は、すべての __修正が完了__ してから __行います__。

## 対戦を行い、通算成績を数える処理の修正

次に、__対戦__ を行い、__通算成績を数える__ 処理を、下記のプログラムのように修正します。

* __7 行目__：__手番を入れ替えない__ 場合の __通算成績__ は __`count_list[0]`__ に代入されるので、元のプログラムの __`count`__ を __`count_list[0]`__ に __修正__ する
* __8 行目__：__手番を入れ替えた場合__ の __AI の関数__ の __list__ は、__`[ai[1], ai[0]]`__ なので、__`play`__ メソッドの __キーワード引数 `ai`__ にその list を代入して呼び出すことで、__手番を入れ替えた対戦__ を行うことができる。__通算成績__ は __`count_list[1]`__ を使って数える

```python
1  def ai_match(ai, match_num=10000):
2      mb = Marubatsu()
3
4      # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
5      count_list = [ defaultdict(int), defaultdict(int)]
6      for _ in range(match_num):
7          count_list[0][mb.play(ai, verbose=False)] += 1
8          count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1
元と同じなので省略
```

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

```diff_python
def ai_match(ai, match_num=10000):
    mb = Marubatsu()

    # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
    count_list = [ defaultdict(int), defaultdict(int)]
    for _ in range(match_num):
-       count[mb.play(ai, verbose=False)] += 1
+       count_list[0][mb.play(ai, verbose=False)] += 1
+       count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1
元と同じなので省略
```
</details>

:::note
7 行目と 8 行目のプログラムを、下記のプログラムのように、__for 文__ による __繰り返し処理__ をつかって __まとめる__ ことができますが、__慣れない__ とプログラムの意味が __分かりづらくなる__ ので、__無理にまとめる必要はない__ でしょう。本記事では下記のようにまとめませんが、まとめるかどうかは各自で判断して下さい。

```python
for _ in range(match_num):
    for i in range(2):
        count_list[i][mb.play(ai=[ai[i], ai[1 - i]], verbose=False)] += 1
```


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

```diff_python
for _ in range(match_num):
-   count_list[0][mb.play(ai, verbose=False)] += 1
-   count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1
+   for i in range(2):
+       count_list[i][mb.play(ai=[ai[i], ai[1 - i]], verbose=False)] += 1
```
</details>

上記のプログラムの意味が分からない人は、__`i`__ が __`0`__ と __`1`__ の __それぞれの場合__ で __3 行目のプログラム__ が行う処理を、下記の表のように __実際に記述__ してみてください。

| `i` | 3 行目のプログラム |
|:-:|:--|
| __`0`__  | `count_list[0][mb.play(ai=[ai[0], ai[1]], verbose=False)] += 1`  |
| __`1`__  | `count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1`  |

表から、__`i` が `1`__ の __場合__ は、元のプログラムの __8 行目と同じ処理__ が行われることがわかります。__`i` が `0`__ の場合は、__`play`__ メソッドの __実引数__ が __`ai=[ai[0], ai[1]]`__ となっている点が __異なります__ が、__`[ai[0], ai[1]]`__ が、__仮引数__ に __`ai`__ に代入された __list__ と全く __同じ要素を持つ list__ であることが __理解__ できれば、元のプログラムの __7 行目と同じ処理__ が行われることがわかります。
:::

:::note
__`[ ai[1], ai[0] ]`__ は、__`ai`__ に代入された __list__  の __要素__ の __順番を逆__ にしたものです。[以前の記事](https://qiita.com/ysgeso/items/6ca248133ff4d59a81d4#%E3%82%B9%E3%83%A9%E3%82%A4%E3%82%B9%E8%A1%A8%E8%A8%98-%E3%81%9D%E3%81%AE-7%E8%B2%A0%E3%81%AE-step-%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%9F%E5%A0%B4%E5%90%88) で説明したように、list のスライス表記に `[::-1]` を記述することで、__list__ __要素__ の __順番を逆__ にした __list__ を __作成__ することができます。従って、先程の 8 行目のプログラムは、下記のように記述できます。

```python
count_list[1][mb.play(ai[::-1], verbose=False)] += 1
```

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

```diff_python
-count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1
+count_list[1][mb.play(ai[::-1], verbose=False)] += 1
```
</details>

なお、上記の記述は、__要素が多い list__ の場合は __簡潔に記述__ できて __便利__ ですが、__慣れない__ とプログラムの __意味が分かりづらくなる__ ので本記事では採用しません。

他にも、__`reverse`__ や __`reversed` メソッド__ を使う方法がありますが、それらを __使いこなす__ ためには、__正しい知識__ が __必要__ で、__間違った使い方__ をすると __バグの原因__ になる __可能性が高い__ ので、今回の記事では紹介しません。それらについては、必要になる場面があれば紹介しようと思います。

:::

## `ratio` と比率の計算の修正

__比率を数える__ ための __変数__ も、__`count_list`__ と __同様__ に、__`ratio_list`__ という __名前__ にし、__入れ替えない場合__ と、__入れ替えた場合__ の __比率__ を __要素__ とする __list__ を __代入__ することにします。

下記は、__`ai_match`__ の中で、__比率__ を __計算__ する部分の __処理__ を抜き出して、__`ratio_list`__ を使って __比率__ を __計算__ するように __修正__ したプログラムです。

* __2 行目__：入れ替えない場合と、入れ替えた場合の __比率__ を __計算する__ ための __空の dict__ を __要素__ とする __list__ を __`ratio_list`__ に __代入__ する
* __3 行目__：__for 文__ による __繰り返し処理__ によって、__入れ替えない場合__ と __入れ替える場合__ の __比率を計算__ する __処理__ を、4、5 行目で __まとめて記述する__
* __4、5 行目__：__`count`__ を __`count_list[i]`__ に、__`ratio`__ を __`ratio_list[i]`__ に __修正__ する


```python
# それぞれの比率を計算し、ratio_list に代入する
ratio_list = [ {}, {} ]
for i in range(2):
    for key in count_list[i]:
        ratio_list[i][key] = count_list[i][key] / match_num
```

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

__for 文__ の __追加__ による __インデントの変化__ は、__修正箇所__ に __含めません__。

```diff_python
# それぞれの比率を計算し、ratio_list に代入する
-ratio = {}
+ratio_list = [ {}, {} ]
+for i in range(2):
-   for key in count.keys():
+   for key in count_list[i]:
-       ratio[i][key] = count[key] / match_num
+       ratio_list[i][key] = count_list[i][key] / match_num
```
</details>

:::note
前回の記事では、上記のプログラムの 3 行目の __for 文__ の __反復可能オブジェクト__ には、下記のプログラムの 1 行目のように __`keys`__ メソッドを __記述__ していましたが、下記の 4 行目のように、__`keys`__ メソッドを __記述しない__ 場合でも __同じ処理__ が行われるので、今回の記事からは、__`keys`__ メソッドの __記述__ を __省略__ することにします。

```python
for key in count.keys():
    for 文のブロックの処理

for key in count:
    for 文のブロックの処理
```
:::

:::note
上記のプログラムの 3 行目と 4 行目の __for 文__ を、下記のプログラムのように __入れ替える__ と __エラーが発生する__ 点に __注意が必要__ です。エラーが発生する __理由__ は、下記のプログラムの __3 行目を実行__ する際に、__`count_list[i]`__ の __`i`__ が __定義されていない__ からです。__元のプログラム__ では、__`for i in range(2)`__ の __ブロックの中__ に、__`count_list[i]`__ が __記述__ されているため __エラー__ は __発生しません__。

```python
# それぞれの比率を計算し、ratio_list に代入する
ratio_list = [ {}, {} ]
for key in count_list[i]:
    for i in range(2):
        ratio_list[i][key] = count_list[i][key] / match_num
```
:::

## 結果の表示部分の修正

下記は、__`ai_match`__ の中で、__結果を表示__ する部分の処理を抜き出して __修正__ したプログラムです。__回数や比率__ を計算する場合と __同様__ に、__for 文__ による __繰り返し処理__ で __処理をまとめる__ ことができます。なお、この直前に記述する __`order_list`__ は __修正__ の __必要__ は __ありません__。

* __2 行目__：__`count`__ を __`count_list`__ に、__`ratio`__ を __`ratio_list`__ に __修正__ する
* __5 行目__：__for 文__ による __繰り返し処理__ によって、__入れ替えない場合__ と __入れ替える場合__ の __結果を表示__ する __処理__ を、6 ~ 8 行目で __まとめて記述する__
* __7月 行目__：__`data`__ を __`data[i]`__ に修正する


```python
1  # 通算成績の回数と比率の表示
2  diff_list = [ ("count", count_list, "d"), ("ratio", ratio_list, ".1%") ]
3  for title, data, format in diff_list:
4      print(title, end="")
5      for i in range(2):
6          for key in order_list:
7              print(f" {key} {data[i][key]:{format}}", end="")
8          print()
```

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

__for 文__ の __追加__ による __インデントの変化__ は、__修正箇所__ に __含めません__。

```diff_python
# 通算成績の回数と比率の表示
-diff_list = [ ("count", count, "d"), ("ratio", ratio, ".1%") ]
+diff_list = [ ("count", count_list, "d"), ("ratio", ratio_list, ".1%") ]
for title, data, format in diff_list:
    print(title, end="")
+   for i in range(2):
        for key in order_list:
-           print(f" {key} {data[key]:{format}}", end="")
+           print(f" {key} {data[i][key]:{format}}", end="")
        print()
```
</details>

上記の __7 行目__ の __修正__ がわかりづらいと思いますので補足します。下記の表は、元のプログラムと、修正後のプログラムで、__`data`__ と __`data[i][key]`__ の部分が __何であるか__ を表します。表から、__7 行目__ の __修正__ によって、__7 行目__ の __`data[i][key]`__ の部分の処理で、__`count`__ が __`count_list[i]`__ に、__`ratio`__ が __`ratio_list[i]`__ に __修正__ されたことがわかります。

|  | 元のプログラム  | 修正後のプログラム  |
|:-:|:-:|:-:|
| __`data`__ の部分 | `count` または<br>`ratio`  | `count_list` または<br> `ratio_list`  |
| __`data[i][key]`__ の部分 | `count[key]` または<br>`ratio[key]`  | `count_list[i][key]` または<br> `ratio_list[i][key]`  |

## `ai_match` の修正

下記は、上記のすべての修正を行った `ai_match` のプログラムです。

```python
from marubatsu import Marubatsu
from collections import defaultdict

def ai_match(ai, match_num=10000):
    mb = Marubatsu()

    # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
    count_list = [ defaultdict(int), defaultdict(int)]
    for _ in range(match_num):
        count_list[0][mb.play(ai, verbose=False)] += 1
        count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1

    # それぞれの比率を計算し、ratio_list に代入する
    ratio_list = [ {}, {} ]
    for i in range(2):
        for key in count_list[i]:
            ratio_list[i][key] = count_list[i][key] / match_num

    # 〇の勝利、×の勝利、引き分けの順番で表示することを表す list 
    order_list = [Marubatsu.CIRCLE, Marubatsu.CROSS, Marubatsu.DRAW]

    # 通算成績の回数と比率の表示
    diff_list = [ ("count", count_list, "d"), ("ratio", ratio_list, ".1%") ]
    for title, data, format in diff_list:
        print(title, end="")
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()
```

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

```diff_python
from marubatsu import Marubatsu
from collections import defaultdict

def ai_match(ai, match_num=10000):
    mb = Marubatsu()

    # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
-   count = defaultdict(int)
+   count_list = [ defaultdict(int), defaultdict(int) ]
    for _ in range(match_num):
-       count[mb.play(ai, verbose=False)] += 1
+       count_list[0][mb.play(ai, verbose=False)] += 1
+       count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1

    # それぞれの比率を計算し、ratio_list に代入する
-   ratio = {}
+   ratio_list = [ {}, {} ]
+   for i in range(2):
-       for key in count:
+       for key in count_list[i]:
-           ratio[i][key] = count[key] / match_num
+           ratio_list[i][key] = count_list[i][key] / match_num

    # 〇の勝利、×の勝利、引き分けの順番で表示することを表す list 
    order_list = [Marubatsu.CIRCLE, Marubatsu.CROSS, Marubatsu.DRAW]

    # 通算成績の回数と比率の表示
-   diff_list = [ ("count", count, "d"), ("ratio", ratio, ".1%") ]
+   diff_list = [ ("count", count_list, "d"), ("ratio", ratio_list, ".1%") ]
    for title, data, format in diff_list:
        print(title, end="")
+       for i in range(2):
            for key in order_list:
-               print(f" {key} {data[key]:{format}}", end="")
+               print(f" {key} {data[i][key]:{format}}", end="")
            print()
```
</details>

下記のプログラムを実行することで、__`ai1` VS `ai2`__ と __`ai2` VS `ai1`__ の __それぞれ__ の __通算成績__ の __回数__ と __比率__ が __表示__ されることが確認できます。

```python
from ai import ai1, ai2

ai_match(ai=[ai1, ai2])
```

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

```
count o 7843 x 1758 draw 399
 o 5234 x 4397 draw 369
ratio o 78.4% x 17.6% draw 4.0%
 o 52.3% x 44.0% draw 3.7%
```

# `ai_match` の改良 その 5（表示をわかりやすくする）

修正した `ai_match` の __表示__ には、いくつか __分かりづらい__ 点があるので __改良__ します。どのような点が分かりづらいかについて少し考えてみて下さい。

## 表示の改良その 1（count と ratio の後で改行する）

先程の実行結果では、1 行目と、3 行目の先頭に、count と ratio が表示されるため、__上下の行__ での数字の __表示位置__ が __ずれてしまう__ せいで __分かりづらく__ なっています。この問題は、下記のプログラムのように、count と ratio の後で __改行__ することで簡単に __解決__ できます。

* __5 行目__：__`print`__ の __実引数__ の __`end=""`__ を __削除__ して __改行しない__ ようにする
```python
1  def ai_match(ai, match_num=10000):
元と同じなので省略
2      # 通算成績の回数と比率の表示
3      diff_list = [ ("count", count_list, "d"), ("ratio", ratio_list, ".1%") ]
4      for title, data, format in diff_list:
5          print(title)
6          for i in range(2):
7              for key in order_list:
8                  print(f" {key} {data[i][key]:{format}}", end="")
9              print()
```

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

```python
def ai_match(ai, match_num=10000):
    mb = Marubatsu()

    # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
    count_list = [ defaultdict(int), defaultdict(int)]
    for _ in range(match_num):
        count_list[0][mb.play(ai, verbose=False)] += 1
        count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1

    # それぞれの比率を計算し、ratio_list に代入する
    ratio_list = [ {}, {} ]
    for i in range(2):
        for key in count_list[i]:
            ratio_list[i][key] = count_list[i][key] / match_num

    # 〇の勝利、×の勝利、引き分けの順番で表示することを表す list 
    order_list = [Marubatsu.CIRCLE, Marubatsu.CROSS, Marubatsu.DRAW]

    # 通算成績の回数と比率の表示
    diff_list = [ ("count", count_list, "d"), ("ratio", ratio_list, ".1%") ]
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()
```
</details>

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

```diff_python
def ai_match(ai, match_num=10000):
元と同じなので省略
    # 通算成績の回数と比率の表示
    diff_list = [ ("count", count_list, "d"), ("ratio", ratio_list, ".1%") ]
    for title, data, format in diff_list:
-       print(title, end="")
+       print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()
```
</details>

下記のプログラムを実行することで、count などの後で __改行__ されることが確認できます。

```python
ai_match(ai=[ai1, ai2])
```
実行結果（実行結果はランダムなので下記とは異なる場合があります）
```
count
 o 7791 x 1800 draw 409
 o 5198 x 4402 draw 400
ratio
 o 77.9% x 18.0% draw 4.1%
 o 52.0% x 44.0% draw 4.0%
```

## 表示の改良その 2（count の回数の表示の文字幅を揃える）

下記は、__`match_num`__ に __`300`__ を指定して __300 回__ ずつ __対戦を行う__ プログラムです。

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

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

```
count
 o 248 x 41 draw 11
 o 152 x 137 draw 11
ratio
 o 82.7% x 13.7% draw 3.7%
 o 50.7% x 45.7% draw 3.7%
```

上記の実行結果では、__2 行目__ の __x の数__ が __`41`__、__3 行目__ の __x の数__ が __`137`__ のように、__桁数が異なる__ ので、その後の __`draw`__ の __表示__ が、2 行目と 3 行目で __ずれて__ しまいます。__上下の行__ で __数字__ の __表示位置__ が __ずれる__ と __わかりづらくなる__ ので、__表示位置__ を __揃える__ ことにします。

数字の __表示位置__ を __揃える__ 最も簡単な方法は、__すべて__ の数字の __表示幅__ を __揃える__ という方法です。そのためには、数字の __表示幅__ を __決める__ 必要があります。

:::note
__o__ と __x__ と __draw__ で、それぞれ __別々に__ 数字の __表示幅__ を __決める__ こともできますが、__処理が面倒__ なわりに、__見やすさ__ が __大きく向上しない__ ので本記事では紹介しません。
:::

### 数字の表示幅の決め方 その 1 （想定される数値の最大値から決める）

__数字の表示幅__ として、__想定__ される __最大__ の __表示幅__ を __設定する__ という方法があります。__`ai_match`__ の場合、表示される __通算成績__ の __回数__ の __数字__ の __最大値__ は、__対戦__ を行う __回数__ です。従って、__対戦__ を行う __回数__ の __最大値__ がわかれば、__表示幅__ の __最大値__ が __決まり__ ます。

__`ai_match`__ では、__`match_num`__ に __任意__ の __対戦回数__ を __設定__ できますが、あまり __大きな回数__ の対戦を __行っても__ 、__`ai_match`__ の __計算に時間がかかる__ だけで、通算成績の __精度__ はほとんど __向上しません__。そこで、多く見積もって、__`match_num`__ の __最大値__ として __`1000000`__（百万）を __想定__ することにします。なお、この百万は、筆者のパソコンで、__`match_num`__ に __`10000`__（一万）を指定して __`ai_match`__ を __実行__ すると、__約 3 秒__ かかることから設定しました。

__`1000000`__ の __文字数__ は __7 文字__ なので、下記のプログラムのように、[前回の記事](https://qiita.com/ysgeso/items/bf42b7b578bc2d5da949#%E6%96%87%E5%AD%97%E5%B9%85) で紹介した、__書式指定__ の __文字幅__ に __7 を指定__ することで、__空白文字__ を __含めて__、__常に__ 回数を表す数値が __7 文字で表示__ されるようになり、__上下__ の数字の __表示位置__ が __必ず揃う__ ようになります。

* __3 行目__：__回数__ の __書式指定__ の __`d`__ を、__文字幅__ に __`7`__ を __指定__ する __`7d`__ に __修正__ する

```python
1  def ai_match(ai, match_num=10000):
元と同じなので省略
2      # 通算成績の回数と比率の表示
3      diff_list = [ ("count", count_list, "7d"), ("ratio", ratio_list, ".1%") ]
4      for title, data, format in diff_list:
5          print(title)
6          for i in range(2):
7              for key in order_list:
8                  print(f" {key} {data[i][key]:{format}}", end="")
9              print()
```

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

```python
def ai_match(ai, match_num=10000):
    mb = Marubatsu()

    # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
    count_list = [ defaultdict(int), defaultdict(int)]
    for _ in range(match_num):
        count_list[0][mb.play(ai, verbose=False)] += 1
        count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1

    # それぞれの比率を計算し、ratio_list に代入する
    ratio_list = [ {}, {} ]
    for i in range(2):
        for key in count_list[i]:
            ratio_list[i][key] = count_list[i][key] / match_num

    # 〇の勝利、×の勝利、引き分けの順番で表示することを表す list 
    order_list = [Marubatsu.CIRCLE, Marubatsu.CROSS, Marubatsu.DRAW]

    # 通算成績の回数と比率の表示
    diff_list = [ ("count", count_list, "7d"), ("ratio", ratio_list, ".1%") ]
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()
```
</details>

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

```diff_python
def ai_match(ai, match_num=10000):
元と同じなので省略    # 通算成績の回数と比率の表示
-   diff_list = [ ("count", count_list, "d"), ("ratio", ratio_list, ".1%") ]
+   diff_list = [ ("count", count_list, "7d"), ("ratio", ratio_list, ".1%") ]
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()
```
</details>

下記のプログラムを実行することで、回数の __表示位置__ が __上下で揃う__ ことが __確認__ できます。なお、 __間が空きすぎる__ 点や、__数字__ と __o や x__ の __間が狭い__ ことが __気になる人__ は、__書式指定__ の __文字幅__ を __`5`__ や __`6`__ に __減らし__ たり、8 行目の __`{key}`__ の __直前__ に __半角の空白文字__ を __追加__ するなどの方法で、__自由__ に __表示__ を自分好みに __調整__ して下さい。

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

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

```
count
 o     232 x      51 draw      17
 o     154 x     139 draw       7
ratio
 o 77.3% x 17.0% draw 5.7%
 o 51.3% x 46.3% draw 2.3%
```

### 数字の表示幅の決め方 その 2 （計算で求める）

上記の、__数値__ の __最大値__ を __想定__ する方法では、上記の表示結果のように、__対戦回数__ が __少ない場合__ は、__多くの空白__ が __表示__ されてしまうという __欠点__ があります。また、__書式指定__ の __文字幅__ は、文字列に変換した際の __文字幅__ の __最低値__ を __指定__ するものなので、__想定以上__ の __回数の対戦__ を行うと、__表示__ が上下で __ずれる場合__ が __あります__。

__計算__ が __必要__ になりますが、__`match_num`__ に __代入__ された __数値__ の __文字列の長さ__ を計算し、それを __書式設定__ の __文字幅__ に __設定__ することで、下記のプログラムのように、__対戦回数__ に __応じた長さ__ で、数字の __表示幅__ を __自動的__ に __調整__ して __表示__ することができるようになります。

* __3 行目__：組み込み関数 __[`str`](https://qiita.com/ysgeso/items/a189461ae32a6531ff7f#%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF%E5%9E%8B%E3%81%AE%E5%A4%89%E6%8F%9B)__ を使って、__`match_num`__ を __文字列型__ のデータに __変換__ し、組み込み関数 __[`len`](https://qiita.com/ysgeso/items/6ca248133ff4d59a81d4#len-%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)__ を使って、その __文字列の長さ__ を __計算__ し、__`width`__ に代入する
* __4 行目__：__[f 文字列](https://qiita.com/ysgeso/items/bf42b7b578bc2d5da949#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88%E6%B8%88%E6%96%87%E5%AD%97%E5%88%97%E3%83%AA%E3%83%86%E3%83%A9%E3%83%ABf-%E6%96%87%E5%AD%97%E5%88%97)__ を使って、__回数__ の数字の __書式指定__ の __文字幅__ に __`width`__ を __設定__ する。__式__ を使って __書式指定__ を __指定__ する方法を忘れた方は、[前回の記事](https://qiita.com/ysgeso/items/bf42b7b578bc2d5da949#%E6%9B%B8%E5%BC%8F%E6%8C%87%E5%AE%9A%E3%81%AE%E9%83%A8%E5%88%86%E3%82%92%E5%BC%8F%E3%81%A7%E8%A8%98%E8%BF%B0%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)を参照すること
 
```python
 1  def ai_match(ai, match_num=10000):
元と同じなので省略
 2      # 通算成績の回数と比率の表示
 3      width = len(str(match_num))
 4      diff_list = [ ("count", count_list, f"{width}d"), ("ratio", ratio_list, ".1%") ]
 5      for title, data, format in diff_list:
 6          print(title)
 7          for i in range(2):
 8              for key in order_list:
 9                  print(f" {key} {data[i][key]:{format}}", end="")
10              print()
```

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

```python
def ai_match(ai, match_num=10000):
    mb = Marubatsu()

    # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
    count_list = [ defaultdict(int), defaultdict(int)]
    for _ in range(match_num):
        count_list[0][mb.play(ai, verbose=False)] += 1
        count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1

    # それぞれの比率を計算し、ratio_list に代入する
    ratio_list = [ {}, {} ]
    for i in range(2):
        for key in count_list[i]:
            ratio_list[i][key] = count_list[i][key] / match_num

    # 〇の勝利、×の勝利、引き分けの順番で表示することを表す list 
    order_list = [Marubatsu.CIRCLE, Marubatsu.CROSS, Marubatsu.DRAW]

    # 通算成績の回数と比率の表示
    width = len(str(match_num))
    diff_list = [ ("count", count_list, f"{width}d"), ("ratio", ratio_list, ".1%") ]
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()
```
</details>

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

```diff_python
def ai_match(ai, match_num=10000):
元と同じなので省略
    # 通算成績の回数と比率の表示
+   width = len(str(match_num))
-   diff_list = [ ("count", count_list, "7d"), ("ratio", ratio_list, ".1%") ]
+   diff_list = [ ("count", count_list, f"{width}d"), ("ratio", ratio_list, ".1%") ]
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()

```
</details>

下記のプログラムを実行することで、__`match_num`__ に __3 桁__ の __`300`__ を指定した際に、__回数__ の数値が、__空白を含め__ て __すべて 3 文字の幅__ で __表示__ されることが __確認__ できます。

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

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

```
count
 o 239 x  48 draw  13
 o 147 x 142 draw  11
ratio
 o 79.7% x 16.0% draw 4.3%
 o 49.0% x 47.3% draw 3.7%
```
 
## 表示の改良その 3（ratio の比率の表示の文字幅を揃える）

上記の表示結果では、`ratio` の __比率__ の __数字__ の __表示__ は __上下__ で __揃って__ いますが、__`x`__ の __`19.7%`__ が __5 文字__、__`draw`__ の __`5.7%`__ が __4 文字__ になっていることからわかるように、__比率__ の __数字__ も __表示幅__ が __異なる場合__ があるため、下記のように、__上下が揃わない可能性__ があります。

```
ratio
 o 50.0% x 5.0% draw 45.0%
 o 30.0% x 50.0% draw 20.0%
```

__比率__ は、__最小値__ が __0.0%__ で、__最大値__ が __100.0%__ なので、__文字幅__ の __最大値__ は __常に `100.0%`__ の __6 文字__ です。そのため、下記のプログラムの 4 行目のように、__書式指定__ で __比率__ の __表示幅__ を __6 に設定__ すれば、__上下__ で __ずれる__ ことは決して __無くなります__。

```python
 1  def ai_match(ai, match_num=10000):
元と同じなので省略
 2      # 通算成績の回数と比率の表示
 3      width = len(str(match_num))
 4      diff_list = [ ("count", count_list, f"{width}d"), ("ratio", ratio_list, "6.1%") ]
 5      for title, data, format in diff_list:
 6          print(title)
 7          for i in range(2):
 8              for key in order_list:
 9                  print(f" {key} {data[i][key]:{format}}", end="")
10              print()
```

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

```python
def ai_match(ai, match_num=10000):
    mb = Marubatsu()

    # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
    count_list = [ defaultdict(int), defaultdict(int)]
    for _ in range(match_num):
        count_list[0][mb.play(ai, verbose=False)] += 1
        count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1

    # それぞれの比率を計算し、ratio_list に代入する
    ratio_list = [ {}, {} ]
    for i in range(2):
        for key in count_list[i]:
            ratio_list[i][key] = count_list[i][key] / match_num

    # 〇の勝利、×の勝利、引き分けの順番で表示することを表す list 
    order_list = [Marubatsu.CIRCLE, Marubatsu.CROSS, Marubatsu.DRAW]

    # 通算成績の回数と比率の表示
    width = len(str(match_num))
    diff_list = [ ("count", count_list, f"{width}d"), ("ratio", ratio_list, "6.1%") ]
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()
```
</details>

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

```diff_python
def ai_match(ai, match_num=10000):
元と同じなので省略
    # 通算成績の回数と比率の表示
+   width = len(str(match_num))
-   diff_list = [ ("count", count_list, f"{width}d"), ("ratio", ratio_list, ".1%") ]
+   diff_list = [ ("count", count_list, f"{width}d"), ("ratio", ratio_list, "6.1%") ]
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()

```
</details>

下記のプログラムを実行することで、__比率__ の __数字__ の __表示幅__ が、__空白を含めて__ すべて __6 文字__ になることが確認できます。

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

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

```
count
 o 241 x  44 draw  15
 o 144 x 146 draw  10
ratio
 o  80.3% x  14.7% draw   5.0%
 o  48.0% x  48.7% draw   3.3%
```

## 表示の改良その 4（count と ratio で上下を揃える）

上記の実行結果では、__count__ と __ratio__ で __x__ と __draw__ の __表示位置__ が __上下__ で __揃わない__ 点が __わかりづらい__ ので、__揃える__ ことにします。その場合は、下記のプログラムのように、__count__ と __ratio__ の __数字__ の __表示幅__ を、__幅の大きい方__ に __揃える__ 必要があります。

* __3 行目__：__実引数__ に記述した式の計算結果の中の __最大値__ を計算して返す __`max`__ という組み込み関数を使って、回数の文字幅である __`len(str(match_num))`__ と 比率の文字幅である __`6`__ の __最大値__ を __計算__ して __`width`__ に __代入__ する
* __4、5 行目__：__f 文字列__ を使って、__比率__ の数字の __書式指定__ の __文字幅__ に __`width`__ を __設定__ する。なお、行の内容が長くなって見づらくなったので __途中__ で __改行__ した

```python
 1  def ai_match(ai, match_num=10000):
元と同じなので省略
 2      # 通算成績の回数と比率の表示
 3      width = max(len(str(match_num)), 6)
 4      diff_list = [ ("count", count_list, f"{width}d"),
 5                    ("ratio", ratio_list, f"{width}.1%") ]
 6      for title, data, format in diff_list:
 7          print(title)
 8          for i in range(2):
 9              for key in order_list:
10                  print(f" {key} {data[i][key]:{format}}", end="")
11              print()
```

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

```python
def ai_match(ai, match_num=10000):
    mb = Marubatsu()

    # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
    count_list = [ defaultdict(int), defaultdict(int)]
    for _ in range(match_num):
        count_list[0][mb.play(ai, verbose=False)] += 1
        count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1

    # それぞれの比率を計算し、ratio_list に代入する
    ratio_list = [ {}, {} ]
    for i in range(2):
        for key in count_list[i]:
            ratio_list[i][key] = count_list[i][key] / match_num

    # 〇の勝利、×の勝利、引き分けの順番で表示することを表す list 
    order_list = [Marubatsu.CIRCLE, Marubatsu.CROSS, Marubatsu.DRAW]

    # 通算成績の回数と比率の表示
    width = max(len(str(match_num)), 7)
    diff_list = [ ("count", count_list, f"{width}d"),
                  ("ratio", ratio_list, f"{width}.1%") ]
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()
```
</details>

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

```diff_python
def ai_match(ai, match_num=10000):
元と同じなので省略
    # 通算成績の回数と比率の表示
-   width = len(str(match_num))
+   width = max(len(str(match_num)), 7)
-   diff_list = [ ("count", count_list, f"{width}d"), ("ratio", ratio_list, "6.1%") ]
+   diff_list = [ ("count", count_list, f"{width}d"),
+                 ("ratio", ratio_list, f"{width}.1%") ]
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()

```
</details>

:::note
`max` の __実引数__ に、__反復可能オブジェクト__ を __記述__ すると、下記のプログラムのように、その __要素__ の中の __最大値__ を計算して返します。

```python
print(max([1, 2, 3])
```

実行結果

```
3
```

なお、__`max`__ の __実引数__ に __反復可能オブジェクト__ を __複数記述__ することは __できません__。

__`max`__ に __類似__ する __組み込み関数__ として、__最小値__ を __計算__ する __`min`__ があります。
:::

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

https://docs.python.org/ja/3/library/functions.html?highlight=sum#max

https://docs.python.org/ja/3/library/functions.html?highlight=sum#min

下記のプログラムを実行することで、__count__ と __ratio__ で、__すべての数字__ の __上下__ の __表示位置__ が __揃う__ ようになることが確認できます。

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

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

```
count
 o    236 x     45 draw     19
 o    154 x    136 draw     10
ratio
 o  78.7% x  15.0% draw   6.3%
 o  51.3% x  45.3% draw   3.3%
```

## 表示の改良その 5（ai の名前の表示）

__表示結果__ の __最初__ に、__対戦__ する __AI__ の __関数の名前__ を __表示__ すると __わかりやすくなる__ と思いませんか？__関数の名前__ は、__関数オブジェクト__ の __`__name__`__ という __特殊属性__ に代入されているので、それを利用することで、AI の関数の名前を表示することができます。

もう一つの改良として、元のプログラムでは、__count__ と __ratio__ の __結果__ が __続けて表示__ される点が、すこし __わかりづらい__ ので、__間で改行を行う__ ことで __見やすくする__ ことにします。

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

* __2 行目__：__AI の関数__ が __代入__ された __`ai[0]`__ と __`ai[1]`__ の __`__name__` 属性__ を __表示__ することで、__対戦__ する __AI__ の __関数の名前__ を __表示__ する
* __15 行目__：__count__ や __ratio__ を __表示__ する __ブロックの最後__ に __`print()`__ を __記述__ して __改行__ する

```python
 1  def ai_match(ai, match_num=10000):
 2      print(f"{ai[0].__name__} VS {ai[1].__name__}")
 3
 4      mb = Marubatsu()
元と同じなので省略
 5       for title, data, format in diff_list:
 6          print(title, end="")
 7          for key in data[0]:
 8              print(f" {key:>{width}}", end="")
 9          print()
10          for i in range(3):
11              print(f"{item_text_list[i]:5}", end="")
12              for value in data[i].values():
13                  print(f" {value:{format}}", end="")
14              print()
15          print()
```

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

```python
def ai_match(ai, match_num=10000):
    print(f"{ai[0].__name__} VS {ai[1].__name__}")

    mb = Marubatsu()

    # ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
    count_list = [ defaultdict(int), defaultdict(int)]
    for _ in range(match_num):
        count_list[0][mb.play(ai, verbose=False)] += 1
        count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1

    # それぞれの比率を計算し、ratio_list に代入する
    ratio_list = [ {}, {} ]
    for i in range(2):
        for key in count_list[i]:
            ratio_list[i][key] = count_list[i][key] / match_num

    # 〇の勝利、×の勝利、引き分けの順番で表示することを表す list 
    order_list = [Marubatsu.CIRCLE, Marubatsu.CROSS, Marubatsu.DRAW]

    # 通算成績の回数と比率の表示
    width = max(len(str(match_num)), 7)
    diff_list = [ ("count", count_list, f"{width}d"),
                  ("ratio", ratio_list, f"{width}.1%") ]
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()
        print()
```
</details>

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

```diff_python
def ai_match(ai, match_num=10000):
+   print(f"{ai[0].__name__} VS {ai[1].__name__}")
 
    mb = Marubatsu()
 元と同じなので省略
    for title, data, format in diff_list:
        print(title)
        for i in range(2):
            for key in order_list:
                print(f" {key} {data[i][key]:{format}}", end="")
            print()
+       print()
```
</details>

下記のプログラムを実行することで、__意図した表示__ が行われることが __確認__ できます。

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

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

```
ai1 VS ai2
count
 o     241 x      49 draw      10
 o     141 x     150 draw       9

ratio
 o   80.3% x   16.3% draw    3.3%
 o   47.0% x   50.0% draw    3.0%
``` 

# 今回の記事のまとめ

長くなったので、今回の記事はここまでにしたいと思います。なお、`ai_match` の改良は次回の記事で完了する予定です。

今回の記事では、__手番を入れ替えた対戦__ を行うように `ai_match` を改良しました。また、__表示__ が __わかりやすく__ なるように改良を行いました。

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

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

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

今回の記事では、marubatsu.py は修正していないので、marubatsu_new.py はありません。

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

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

# 次回の記事

https://qiita.com/ysgeso/items/729c5425573cf1e8051f