0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

0
Last updated at Posted at 2024-01-04

目次と前回の記事

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

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

これまでに作成した 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 を代入 することにします。

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) ]
元と同じなので省略

上記を count_list = [defaultdict(int)] * 2 のように記述すると、以前の記事で説明したように、count_list の 0 番の要素 と、1 番の要素 が、同一の 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) ]
元と同じなので省略

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

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

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

  • 7 行目:手番を入れ替えない 場合の 通算成績 は count_list[0] に代入されるので、元のプログラムの count を count_list[0] に 修正 する
  • 8 行目:手番を入れ替えた場合 の AI の関数 の list は、[ai[1], ai[0]] なので、play メソッドの キーワード引数 ai にその list を代入して呼び出すことで、手番を入れ替えた対戦 を行うことができる。通算成績 は count_list[1] を使って数える
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
元と同じなので省略
修正箇所
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
元と同じなので省略

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

for _ in range(match_num):
    for i in range(2):
        count_list[i][mb.play(ai=[ai[i], ai[1 - i]], verbose=False)] += 1
修正箇所
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

上記のプログラムの意味が分からない人は、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 行目と同じ処理 が行われることがわかります。

[ ai[1], ai[0] ] は、ai に代入された list の 要素 の 順番を逆 にしたものです。以前の記事 で説明したように、list のスライス表記に [::-1] を記述することで、list 要素 の 順番を逆 にした list を 作成 することができます。従って、先程の 8 行目のプログラムは、下記のように記述できます。

count_list[1][mb.play(ai[::-1], verbose=False)] += 1
修正箇所
-count_list[1][mb.play(ai=[ai[1], ai[0]], verbose=False)] += 1
+count_list[1][mb.play(ai[::-1], verbose=False)] += 1

なお、上記の記述は、要素が多い 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] に 修正 する
# それぞれの比率を計算し、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
修正箇所

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

# それぞれの比率を計算し、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

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

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

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

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

# それぞれの比率を計算し、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] に修正する
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()
修正箇所

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

# 通算成績の回数と比率の表示
-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()

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

元のプログラム 修正後のプログラム
data の部分 count または
ratio
count_list または
ratio_list
data[i][key] の部分 count[key] または
ratio[key]
count_list[i][key] または
ratio_list[i][key]

ai_match の修正

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

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()
修正箇所
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()

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

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="" を 削除 して 改行しない ようにする
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()
行番号のないプログラム
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()
修正箇所
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()

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

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 回 ずつ 対戦を行う プログラムです。

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 行目で ずれて しまいます。上下の行 で 数字 の 表示位置 が ずれる と わかりづらくなる ので、表示位置 を 揃える ことにします。

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

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

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

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

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

1000000 の 文字数 は 7 文字 なので、下記のプログラムのように、前回の記事 で紹介した、書式指定 の 文字幅 に 7 を指定 することで、空白文字 を 含めて、常に 回数を表す数値が 7 文字で表示 されるようになり、上下 の数字の 表示位置 が 必ず揃う ようになります。

  • 3 行目:回数 の 書式指定 の d を、文字幅 に 7 を 指定 する 7d に 修正 する
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()
行番号のないプログラム
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()
修正箇所
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()

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

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 を使って、match_num を 文字列型 のデータに 変換 し、組み込み関数 len を使って、その 文字列の長さ を 計算 し、width に代入する
  • 4 行目:f 文字列 を使って、回数 の数字の 書式指定 の 文字幅 に width を 設定 する。式 を使って 書式指定 を 指定 する方法を忘れた方は、前回の記事を参照すること
 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()
行番号のないプログラム
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()
修正箇所
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()

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

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 に設定 すれば、上下 で ずれる ことは決して 無くなります。

 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()
行番号のないプログラム
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()
修正箇所
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()

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

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 を 設定 する。なお、行の内容が長くなって見づらくなったので 途中 で 改行 した
 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()
行番号のないプログラム
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()
修正箇所
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()

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

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

実行結果

3

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

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

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

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

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() を 記述 して 改行 する
 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()
行番号のないプログラム
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()
修正箇所
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()

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

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 のファイルを見ることができます。

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

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

次回の記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?