0
0

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

Last updated at Posted at 2024-01-04

目次と前回の記事

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

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

これまでに作成した AI

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

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

ai_match の改良 その 4(手番を入れ替えた対戦)

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

〇× ゲームを遊んでみるとすぐにわかると思いますが、〇× ゲーム は、〇 の手番× の手番有利不利大きく変わる ゲームです。そのため、AI どうし強さを測る ためには、〇 と × の担当入れ替え た対戦も 行う必要 がありますが、ai_match片方 の対戦 しか行いません。例えば ai1ai2 の場合は、ai1 VS ai2ai2 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_list0 番の要素 と、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] に代入されるので、元のプログラムの countcount_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

上記のプログラムの意味が分からない人は、i01それぞれの場合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

表から、i1場合 は、元のプログラムの 8 行目と同じ処理 が行われることがわかります。i0 の場合は、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 の場合は 簡潔に記述 できて 便利 ですが、慣れない とプログラムの 意味が分かりづらくなる ので本記事では採用しません。

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

ratio と比率の計算の修正

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

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

  • 2 行目:入れ替えない場合と、入れ替えた場合の 比率計算する ための 空の dict要素 とする listratio_list代入 する
  • 3 行目for 文 による 繰り返し処理 によって、入れ替えない場合入れ替える場合比率を計算 する 処理 を、4、5 行目で まとめて記述する
  • 4、5 行目countcount_list[i] に、ratioratio_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 行目countcount_list に、ratioratio_list修正 する
  • 5 行目for 文 による 繰り返し処理 によって、入れ替えない場合入れ替える場合結果を表示 する 処理 を、6 ~ 8 行目で まとめて記述する
  • 7月 行目datadata[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 行目修正 がわかりづらいと思いますので補足します。下記の表は、元のプログラムと、修正後のプログラムで、datadata[i][key] の部分が 何であるか を表します。表から、7 行目修正 によって、7 行目data[i][key] の部分の処理で、countcount_list[i] に、ratioratio_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 ai2ai2 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_num300 を指定して 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 の数413 行目x の数137 のように、桁数が異なる ので、その後の draw表示 が、2 行目と 3 行目で ずれて しまいます。上下の行数字表示位置ずれるわかりづらくなる ので、表示位置揃える ことにします。

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

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

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

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

ai_match では、match_num任意対戦回数設定 できますが、あまり 大きな回数 の対戦を 行ってもai_match計算に時間がかかる だけで、通算成績の 精度 はほとんど 向上しません。そこで、多く見積もって、match_num最大値 として 1000000(百万)を 想定 することにします。なお、この百万は、筆者のパソコンで、match_num10000(一万)を指定して 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間が狭い ことが 気になる人 は、書式指定文字幅56減らし たり、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_num3 桁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比率数字表示上下揃って いますが、x19.7%5 文字draw5.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 で上下を揃える)

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

  • 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 があります。

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

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

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 の関数の名前を表示することができます。

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

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

  • 2 行目AI の関数代入 された ai[0]ai[1]__name__ 属性表示 することで、対戦 する AI関数の名前表示 する
  • 15 行目countratio表示 する ブロックの最後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