LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その30 その他の座標を表すデータ構造

Last updated at Posted at 2023-11-23

目次と前回の記事

実装の進捗状況と前回までのおさらい

〇×ゲームの仕様と進捗状況

  1. 正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
  2. ゲーム開始時には、ゲーム盤のすべてのマスは空になっている
  3. 2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
  4. 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
  5. 先手は 〇 のプレイヤーである
  6. プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
  7. すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする

仕様の進捗状況は、以下のように表記します。

  • 実装が完了した部分を 背景が灰色の長方形 で記述する
  • 実装の一部が完了した部分を、太字 で記述する

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

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

なお、前回の記事で採用しなかった関数は、test.py から削除してあります。

前回までのおさらい

前回の記事では、座標を表すデータ構造として、列も行も整数で表現し、その 2 つを並べた文字列で表現するというデータ構造と、それを処理するアルゴリズムを紹介しました。

今回の記事では、これまでに紹介したデータ構造と大きく異なるデータ構造と、それを処理するアルゴリズムを紹介します。

数値座標

これまでに紹介した、xy 座標、Excel 座標、xy 文字座標は、いずれも x 座標と y 座標という、2 つのデータ座標を表現 していました。この方法は、数学2 次元の座標同じ方法 で座標を表現するので、直観的に分かりやすい という 利点 がある反面、2 つのデータを記述する必要があるため、記述が長く なってしまうという 欠点 があります。

ゲーム盤の座標 を表すデータ構造では、すべてのマス座標 に、異なるデータ割り当て られていれば、それぞれのマスに どのようなデータ を割り当てても かまいません。例えば、〇×ゲームのマスの座標として、下図のように、それぞれのマスの座標に 法則性のない でらめな データを割り当てる ことは 可能 です。ただし、図を見ればわかると思いますが、このようなデータ構造は、非常に 扱いづらい ので使われることは無いでしょう。

そこで、〇×ゲームのような、2 次元の座標を表すデータ構造構造では、座標 を表す データ を、一般的 に以下のような 性質 を持つように割り当てます。

  • 同じデータ型統一 する
  • 何らかの規則で、規則正しく 割り当てる

同じデータ型で統一する 理由 は、プログラムでは、データ型ごと に、行うことができる 処理が異なる からです。例えば、数値型 のデータは、+ 演算子によって 加算 という 処理 を行うことができますが、文字列型 のデータや、list は数値ではないので 加算 を行うことは できず+ 演算子によって、結合 という、全く異なる処理 が行われます。データ型を統一 することで、すべてのデータ に対して 同じ処理 を行うことができるようになります。

規則正しく 割り当てる理由は、繰り返し の処理を行ったり、計算 を行うことによって座標を 別の 座標を表す データ構造変換 することができるようになるからです。

これまでに紹介 してきた xy 座標、Excel 座標、xy 文字座標は、いずれも 同じデータ型で 統一 しており、データを 規則正しく 割り当てています。

今回の記事では、座標を表すデータを、下記のような性質を持つ、1 つ整数型 のデータで表現する データ構造 を紹介します。

  • 1 つ整数型 のデータで表現する
  • 左上のマス の座標を 0 とし、右方向 にマスが ずれる1 ずつ増える ように割り当てる。ただし、右端 のマスと、次の行の左端 のマスが つながっている ものとして考える

下図は、上記の方法で 〇× ゲームのマスの座標にデータを割り当てたものです。上記の 方法で座標を表現する データ構造 の事を、本記事では 数値座標 と表記することにします。

他にも、「左上のマスを 1 にする」、「下方向にマスがずれると 1 ずつ増やす」、「2 ずつ増やす」など、無数の 割り当ての 方法 が考えられます。ただし、割り当ての 方法によって、数値座標を表すデータが、負の整数 や、2 桁以上の整数 になる場合があります。そのような場合は、この後で説明する、複数数値座標直接連結 する データ構造使えなくなる 点に 注意 する必要があります。

利点と欠点

上記の数値座標の利点と欠点は以下の通りです。

利点

  • 座標を 1 文字短く記述 できる

欠点

  • 慣れないと 座標の 意味分かりづらい

これまでに紹介 してきた 〇×ゲームマスの座標 を表す データ構造 は、いずれも プログラムで 記述 する際に、2 文字以上 で記述する必要がありますが、数値座標 では、1 文字短く記述 できるという 利点 があります。

一方、数値座標は、慣れるまで は、それを見ただけでどのマスを表しているのかが わかりづらい という 欠点 があります。〇×ゲームのような 小さい ゲーム盤の場合は マスの数が少ない ので、人によって慣れる ことでこの 欠点解消 できる かもしれません

複数の数値座標を list で表現するデータ構造

次に、複数数値座標 を表現する データ構造 と、それを処理する アルゴリズム をいくつか紹介します。

複数数値座標 を表現する データ構造 の 1 つに、list を使う 方法があります。これは、以前の記事複数xy 座標 を表現した方法と 同じ です。下記のプログラムは、list を使って複数の数値座標を記述した場合のテストケースです。慣れないと 数値座標の 意味が分かりづらい 点を 除けば、比較的 簡潔に記述 することができます。

from marubatsu import Marubatsu

testcases = [
    # 〇 の勝利のテストケース
    [                      # 着手順とゲーム盤
        [ 0, 3, 1, 4, 2 ], # 135  ooo
        Marubatsu.CIRCLE,  # 24.  xx. 
    ],                     # ...  ...
    # × の勝利のテストケース
    [                          # 着手順とゲーム盤
        [ 3, 0, 4, 1, 6, 2 ],  # 246  xxx 
        Marubatsu.CROSS,       # 13.  oo.
    ],                         # 5..  ...
    # 引き分けのテストケース
    [                                  # 着手順とゲーム盤
        [ 0, 3, 1, 4, 5, 2, 6, 7, 8 ], # 136  oox 
        Marubatsu.DRAW,                # 245  xxo
    ],                                 # 789  oxo
]

num_to_xy の定義その 1(if 文を使う方法)

数値座標利用 するために、以前の記事 で、Excel 座標を xy 座標に 変換 する 関数を定義 した場合と 同様 に、数値座標xy 座標変換 する、num_to_xy という名前の 関数を定義 することにします。この関数の定義の方法についていくつか紹介します。

if 文 を使って num_to_xy を定義する方法は、以前の記事 で紹介したif 文を使って excel_to_xy を実装する方法と ほぼ同じ で、下記のプログラムのように定義できます。

def num_to_xy(coord):
    if coord == 0:
        return 0, 0
    elif coord == 1:
        return 0, 1
    elif coord == 2:
        return 0, 2
    elif coord == 3:
        return 1, 0
    elif coord == 4:
        return 1, 1
    elif coord == 5:
        return 1, 2
    elif coord == 6:
        return 2, 0
    elif coord == 7:
        return 2, 1
    elif coord == 8:
        return 2, 2
    else:
        print("invalid coord", coord)

test_judge は下記のように、x, y = num_to_xy(coord) の部分だけを修正します。
下記の修正箇所の、修正元のプログラムは、以前記事 で、xy 座標でテストケースを記述した場合の test_judge です。testdata代入 されたデータは list なので、for 文反復可能オブジェクト には、testdataそのまま記述 することができます。

def test_judge(testcases):
    for testcase in testcases:
        testdata, winner = testcase
        mb = Marubatsu()
        for coord in testdata:
            x, y = num_to_xy(coord)
            mb.move(x, y)
        print(mb)

        if mb.judge() == winner:
            print("ok")
        else:
            print("error!")
修正箇所
def test_judge(testcases):
    for testcase in testcases:
        testdata, winner = testcase
        mb = Marubatsu()
        for coord in testdata:
-           x, y = coord_to_xy(coord)
+           x, y = num_to_xy(coord)
            mb.move(x, y)
        print(mb)

        if mb.judge() == winner:
            print("ok")
        else:
            print("error!")

下記のプログラムを実行することで、修正したデータ構造で、test_judge が正しく動作することが確認できます。

test_judge(testcases)

実行結果

Turn x
ox.
ox.
o..

ok
Turn o
xoo
xo.
x..

ok
Turn x
oxo
oxx
xoo

ok

確認作業の大切さ

上記の実行結果を見て、正しく処理が行われたことが確認できたと 思った方はいませんかそれは本当ですか?そう思った方は、もう一度 実行結果を ゆっくり見て下さい

実行結果を よく見る と、上の 2 つ のゲーム盤の表示が、本当 は 〇 と × が 横に 並んでいなければならないのに、縦に 並んでいる点が おかしい ことがわかるはずです。

人間は、同じような状況何度も体験 すると、慣れ が生じて、確認を怠ってしまう 傾向があります。イソップ童話の、オオカミ少年 の逸話を思い出してください。これまでの記事で、何度も test_judge メソッドを実行し、そのたび正しい動作 が行われることを 確認 してきました。そのため、test_judge を実行して それらしい表示 が行われると、それが正しい動作が行われたことによる表示であるという 思い込み が生まれてしまうようになります。恥ずかしながら、筆者 もこの記事を 最初に記述 した際は、このバグに 気づきませんでした。このバグは、この記事を公開する前に 見直した際 に気づいたので、確認作業の大切さ改めて実感 しました。

人間である以上、このような慣れを 完全に無くす ことは 不可能 ですが、確認作業の際には、このようなことが起きる 可能性がある ということを 念頭に置く ようにすることで、このような見落としを 減らす ことができるようになります。

プログラムをコピーして入力する際の注意点

筆者が、上記のような バグ発生 させた 原因 について説明します。

プログラムでは、上記の num_to_xy のように、過去に記述 した excel_to_xy のような関数と、同様の処理 を行うプログラムを 記述する 場面が 良くあります。そのような場合は、excel_to_xyコピー し、関数の名前や条件式などの、異なる部分を修正 するという方法が良く行われます。

この方法は、プログラムを 効率的に入力 できるので、頻繁に 行われますが、その際に、異なる部分を 正しく修正 しなければ 思わぬエラーが発生 する点に 注意 して下さい。

下記のプログラムは、元の excel_to_xy の定義です。

def excel_to_xy(coord):
    if coord == "A1":
        return 0, 0
    elif coord == "A2":
        return 0, 1
    elif coord == "A3":
        return 0, 2
    elif coord == "B1":
        return 1, 0
    elif coord == "B2":
        return 1, 1
    elif coord == "B3":
        return 1, 2
    elif coord == "C1":
        return 2, 0
    elif coord == "C2":
        return 2, 1
    elif coord == "C3":
        return 2, 2
    else:
        print("invalid coord", coord)

このプログラムを コピー して num_to_xy を記述 する際に、下記のプログラムのように、条件式上から順番coord == 0coord == 1coord == 2 のように修正する人が 多いのではないか と思います。実は、筆者も この記事を書く際に、最初は そのように 修正 してしまいました。

def num_to_xy(coord):
    if coord == 0:
        return 0, 0
    elif coord == 1:
        return 0, 1
    elif coord == 2:
        return 0, 2
    elif coord == 3:
        return 1, 0
    elif coord == 4:
        return 1, 1
    elif coord == 5:
        return 1, 2
    elif coord == 6:
        return 2, 0
    elif coord == 7:
        return 2, 1
    elif coord == 8:
        return 2, 2
    else:
        print("invalid coord", coord)
修正箇所
-def excel_to_xy(coord):
+def num_to_xy(coord):
-   if coord == "A1":
+   if coord == 0:
        return 0, 0
-   elif coord == "A2":
+   elif coord == 1:
        return 0, 1
-   elif coord == "A3":
+   elif coord == 2:
        return 0, 2
-   elif coord == "B1":
+   elif coord == 3:
        return 1, 0
-   elif coord == "B2":
+   elif coord == 4:
        return 1, 1
-   elif coord == "B3":
+   elif coord == 5:
        return 1, 2
-   elif coord == "C1":
+   elif coord == 6:
        return 2, 0
-   elif coord == "C2":
+   elif coord == 7:
        return 2, 1
-   elif coord == "C3":
+   elif coord == 8:
        return 2, 2
    else:
        print("invalid coord", coord)

このバグの原因は、数字順番に並ぶ のが 自然 だという、人間の 先入観原因 です。

元の excel_to_xy では、if 文で、"A1""A2""A3"順番 で Excel 座標を チェック しましたが、"A2"対応 する 数値座標 は、1 ではなく、4 です。元の excel_to_xy では、左上のマスから 下方向 にマスを調べているのに対し、数値座標は、左上のマスから 右方向 に数値を増やしている点が 嚙み合っていない ことがこのバグの原因です。

このバグを修正するためには、下記のプログラムのように、if 文の条件式に、return 文で返す xy 座標対応 した 数値座標を記述 するという方法があります。

def num_to_xy(coord):
    if coord == 0:
        return 0, 0
    elif coord == 3:
        return 0, 1
    elif coord == 6:
        return 0, 2
    elif coord == 1:
        return 1, 0
    elif coord == 4:
        return 1, 1
    elif coord == 7:
        return 1, 2
    elif coord == 2:
        return 2, 0
    elif coord == 5:
        return 2, 1
    elif coord == 8:
        return 2, 2
    else:
        print("invalid coord", coord)
修正箇所
def excel_to_xy(coord):
    if coord == 0:
        return 0, 0
-   elif coord == 1:
+   elif coord == 3:
        return 0, 1
-   elif coord == 2:
+   elif coord == 6:
        return 0, 2
-   elif coord == 3:
+   elif coord == 1:
        return 1, 0
    elif coord == 4:
        return 1, 1
-   elif coord == 5:
+   elif coord == 7:
        return 1, 2
-   elif coord == 6:
+   elif coord == 2:
        return 2, 0
-   elif coord == 7:
+   elif coord == 5:
        return 2, 1
    elif coord == 8:
        return 2, 2
    else:
        print("invalid coord", coord)

下記のプログラムを実行することで、修正したデータ構造で、test_judge が正しく動作することが確認できます。

test_judge(testcases)

実行結果

Turn x
ooo
xx.
...

ok
Turn o
xxx
oo.
o..

ok
Turn x
oox
xxo
oxo

ok

チェックする 数値座標順番012 のように 1 ずつ増やしたい 場合は、下記のプログラムのように、return 文 で返す xy 座標を修正 するという方法も考えられます。どちらの方法でも 正しい処理 が行われるので、自分が わかりやすいと思った ほうを 採用 すれば良いでしょう。

def num_to_xy(coord):
    if coord == 0:
        return 0, 0
    elif coord == 1:
        return 1, 0
    elif coord == 2:
        return 2, 0
    elif coord == 3:
        return 0, 1
    elif coord == 4:
        return 1, 1
    elif coord == 5:
        return 2, 1
    elif coord == 6:
        return 0, 2
    elif coord == 7:
        return 1, 2
    elif coord == 8:
        return 2, 2
    else:
        print("invalid coord", coord)
修正箇所
def num_to_xy(coord):
    if coord == 0:
        return 0, 0
    elif coord == 1:
-       return 0, 1
+       return 1, 0
    elif coord == 2:
-       return 0, 2
+       return 2, 0
    elif coord == 3:
-       return 1, 0
+       return 0, 1
    elif coord == 4:
        return 1, 1
    elif coord == 5:
-       return 1, 2
+       return 2, 1
    elif coord == 6:
-       return 2, 0
+       return 0, 2
    elif coord == 7:
-       return 2, 1
+       return 1, 2
    elif coord == 8:
        return 2, 2
    else:
        print("invalid coord", coord)

下記のプログラムを実行することで、修正したデータ構造で、test_judge が正しく動作することが確認できます。実行結果は先程と同様になるので省略します。

test_judge(testcases)

num_to_xy の定義その 2(テーブルを使う方法)

次に、テーブル を使って実装する方法を考えることにします。数値座標では、座標を 0 以上の整数 で表現しているので、下記のプログラムのように、dict ではなく、list を使うことにします。なお、座標を負の整数や、整数でない数値を使って表現する場合は dict を使う必要があります。

分かりやすさ を重視して、下記のプログラムでは、num_to_xy_table の要素を、ゲーム盤の マスの配置に対応 させて、3 つの要素ごとに 改行 して記述していますが、9 つの要素を改行せずに並べても、1 つの要素ごとに改行してもかまいません。

def num_to_xy(coord):
    num_to_xy_table = [
        [0, 0], [1, 0], [2, 0],
        [0, 1], [1, 1], [2, 1],
        [0, 2], [1, 2], [2, 2],
    ]
    return num_to_xy_table[coord]

先程の if 文の場合と 同様の理由 で、以前の記事excel_to_xy_table では、キーを "A1""A2" のように 縦方向 に順番に並べて記述していましたが、num_to_xy_table の要素は [0, 0][1, 0] のように 横方向 の順番に並べる必要があります。

先程と同様に、excel_to_xy_table のデータを コピー し、その 中身を修正 するという方法で、num_to_xy_table の中身を 記述する 場合は、その点に 気を付けて修正 しないと 間違ったデータ になってしまう点に注意して下さい。

下記のプログラムを実行することで、修正した test_judge が正しく動作することが確認できます。実行結果は先程と同様になるので省略します。

test_judge(testcases)

num_to_xy の定義その 3 (計算を使う方法)

下図のように、〇×ゲームのマスに数値座標を 規則正しく 割り当てた場合は、テーブル使わず に、計算によって 数値座標を xy 座標に 変換 することができます。その方法について少し考えてみて下さい。なお、特定の法則によって規則正しく並んでいる数字に対する計算を行う方法が わからない 場合は、図を書いて考える ことをお勧めします。

上図から、y 座標1 増える と、数値座標3 増える ことがわかります。また、この 3 という 数字 は、ゲーム盤の横のサイズ を表す数字です。従って、数値座標ゲーム盤のサイズ除算(割り算)した時の y 座標 になります。

python では、// という演算子を使うことで、整数 である割り算の 計算 することができます。5 // 3 の計算結果は 1 になります。/ では、1 / 3 のように、割り切れない場合に 整数にならない 点に注意して下さい。従って、y 座標coord // 3 によって計算できます。

x 座標0 の場合の 数値座標 は図からわかるように、3 の倍数 になるので、3 で割った時の 余り0 になります。x 座標1 の場合の 数値座標 は、左隣 の数値座標に 1 を足した ものになるので、3 で割った時の 余り1 になります。x 座標 が 2 の場合も同様で、3 で割った時の 余り2 になります。従って、x 座標 は、数値座標3 で割った 時 の 余り になります。

python では % という演算子を使うことで、割り算の 余り計算 することができます。従って、x 座標 は、coord % 3 によって計算できます。

プログラムで 数字1 からではなく、0 から数える理由 の 1 つは、このような計算を行うことができるからです。

下記のプログラムは、num_to_xy を上記の計算を使うように修正したプログラムです。テーブル必要なくなった ので、プログラムを 短く記述 できます。なお、num_to_xy の内容はほとんどすべてが修正されているので、修正箇所は示しません。

def num_to_xy(coord):
    x = coord % 3
    y = coord // 3
    return x, y

下記のように、num_to_xy1 行で記述 することもできますが、プログラムが わかりにくくなる という欠点があります。

def num_to_xy(coord):
    return coord % 3, coord // 3

下記のプログラムを実行することで、修正した test_judge が正しく動作することが確認できます。実行結果は先程と同様になるので省略します。

test_judge(testcases)

数値座標 を使って 2 次元の座標 を表現する場合は、上記のように、x 座標と y 座標を 簡単に計算 できるような、規則正しい法則 で座標を表すデータを決めると良い。

下図のように、縦方向 に数値座標を表す整数を増やした場合の num_to_xy の定義は下記のプログラムのように、x 座標と y 座標の計算が逆になります。

なお、num_to_xy の定義を変更すると、この後のプログラムの 実行結果おかしくなってしまう ので、下記のプログラムでは、関数名を num_to_xy_2 という名前にしました。

def num_to_xy_2(coord):
    x = coord // 3
    y = coord % 3
    return x, y
修正箇所
def num_to_xy_2(coord):
-   x = coord % 3
+   x = coord // 3
-   y = coord // 3
+   y = coord % 3
    return x, y

興味と時間の余裕がある方は、1 から数え始めた場合など、他の数値座標に対する num_to_xy の実装方法についても考えてみて下さい。

複数の数値座標を文字列で表現するデータ構造

Excel 座標や xy 数値座標のように、複数数値座標文字列表現 することもできます。その場合は、前回の記事 と同様に、数値座標直接連結 した 文字列 で表現する方法と、"," などの 文字列で区切って連結 する方法があります。

数値座標を直接連結するデータ構造

下記のプログラムは、数値座標直接連結 した 文字列記述 した場合のテストケースです。文字列の長さ が、座標の数等しく なるので、これまでの中で 最も短く記述 できます。

testcases = [
    # 〇 の勝利のテストケース
    [                      # 着手順とゲーム盤
        "03142",           # 135  ooo
        Marubatsu.CIRCLE,  # 24.  xx. 
    ],                     # ...  ...
    # × の勝利のテストケース
    [                      # 着手順とゲーム盤
        "304162",          # 246  xxx 
        Marubatsu.CROSS,   # 13.  oo.
    ],                     # 5..  ...
    # 引き分けのテストケース
    [                      # 着手順とゲーム盤
        "031452678",       # 136  oox 
        Marubatsu.DRAW,    # 245  xxo
    ],                     # 789  oxo
]

数値座標直接連結 した場合は、test_judge メソッドの for 文反復可能オブジェクト に、数値座標を連結した 文字列そのまま記述 することができます。その理由は、文字列型 のデータを for 文反復可能オブジェクト の部分に 記述 した場合は、文字列の 先頭の文字 から 順番 に for 文の 変数に代入 して 繰り返し処理 が行われるからです。下記のプログラムは、for 文を使って "ABC" という文字列の先頭の文字から順番に表示するプログラムです。

for txt in "ABC":
    print(txt)

実行結果

A
B
C

従って、test_judge 修正せずテストを行う ことができると 思うかもしれません が、下記のプログラムを実行すると、エラーが発生 します。その理由について少し考えてみて下さい。

test_judge(testcases)

実行結果

略
c:\Users\ys\ai\marubatsu\030\marubatsu.ipynb セル 18 line 2
      1 def num_to_xy(coord):
----> 2     return coord % 3, coord // 3

TypeError: not all arguments converted during string formatting

実行結果のエラーメッセージは、文字列型 のデータに対して % 演算子 で演算を行う際に 表示される ものです。これは 古いバージョン の Python で、文字列の書式化 を行う際に % 演算子使われていた 時の 名残り のようなものです。現在のバージョン の Python でも、文字列の書式化に % 演算子を 利用することはできます が、現在では文字列の書式化 には 別の もっと便利な 方法が使われる ので、このエラーメッセージについての説明は省略します。ただし、古い ウェブページや書籍などで、% 演算子を使った文字列の書式化が 使われている場合がある ので、そのようなプログラムを理解する必要が生じた場合は下記のページのリンク先の説明などを見て下さい。

現在使われている文字列の書式化の方法については必要になった時点で紹介します。

文字列型のデータを for 文に記述した際の注意点

エラーの原因は、num_to_xy の実引数 coord に、文字列型 のデータが 代入 されていることにあります。coord に文字列型のデータが代入される 原因 について説明します。

下記のプログラムのように、文字列型"012"for 文反復可能オブジェクト に記述し、繰り返しのたびに 取り出される値代入 された i を表示 すると、実行結果のように、012 が表示されます。そのため、i には 数値型 のデータが 代入 されるように 見えるかもしれません が、実際には 文字列型 のデータが代入されます。

for i in "012":
    print(i)

実行結果

0
1
2

文字列型 のデータを for 文反復可能オブジェクト記述 した場合は、取り出される値の データ型常に文字列型 のデータになる。

このことは、勘違いしやすくエラーの原因なりやすい 点に 注意 して下さい。

このことは、実引数 に記述されたデータの データ型返り値として返す、組み込み関数 type を使って、下記のプログラムを実行することで 確認 することができます。type の返り値を print で表示すると、下記の実行結果のように <class 'str'> が表示されます。これは、i に代入された値が、文字列型 を表す str という クラスインスタンス であることを表します。

for i in "012":
    print(i, type(i))

実行結果

0 <class 'str'>
1 <class 'str'>
2 <class 'str'>

参考までに、for 文の反復可能オブジェクトに range を記述 した場合は、i数値型 のデータ(int のインスタンス)が 代入 されることを、下記のプログラムで確認できます。

for i in range(3):
    print(i, type(i))

実行結果

0 <class 'int'>
1 <class 'int'>
2 <class 'int'>

type に関する詳細は、下記のリンク先を参照して下さい。

この問題を解決する方法として、以下の 2 種類の方法があります。どちらの方法を選択すべきかについて少し考えてみて下さい。

  • num_to_xy実引数を記述 する際に、int を使って 整数型に変換 する
  • num_to_xyブロックの中 で、int を使って 仮引数 coord整数型に変換 する

前者 の方法は、num_to_xy をプログラムの 複数の場所 から 呼び出す 場合、その すべてで修正 を行う 必要 がありますが、後者 の方法は、num_to_xyブロックの中データ型の変換 の処理を行っているので、num_to_xyブロック以外 のプログラムを 修正 する 必要はありません。従って、一般的には、後者の修正 を行ったほうが良いでしょう。

下記のプログラムは、num_to_xy のブロックの 2 行目で int を使って 仮引数 coord の値を 整数型変換 する処理を 追加 するという修正を行っています。

def num_to_xy(coord):
    coord = int(coord)
    x = coord % 3
    y = coord // 3
    return x, y
修正箇所
def num_to_xy(coord):
+   coord = int(coord)
    x = coord % 3
    y = coord // 3
    return x, y

下記のプログラムを実行することで、修正した test_judge が正しく動作することが確認できます。実行結果は先程と同様になるので省略します。

test_judge(testcases)

数値座標を直接連結するデータ構造の注意点

数値座標直接連結 した文字列で表現する データ構造 は、〇×ゲームのように、マスの数10 未満場合 でしか使えません。マスの数が 10 以上 の場合は、数値座標2 桁以上 になる場合があるため、どこで 数値座標が 区切られているかわからなくなる からです。例えば、"12" が記述された場合に、122 つ数値座標 が記述されているのか、12 という 1 つ数値座標 が記述されているのかを 区別 することは できません

そのような場合は、すべて数値座標"02" のように、2 桁の文字列 で記述するという方法も考えられますが、その場合は、数値座標並べた 場合に "123456" のように、どこで 数値座標が 区切られ るかが わかりづらくなる という欠点があります。

従って、2 桁以上 の数値座標を表記する 必要がある 合は、次に説明する、数値座標を "," で区切って連結 するデータ構造を 採用したほうが良い でしょう。

利点と欠点

上記の事から、このデータ構造の利点と欠点は以下のようになります。

利点

  • 最も短く 記述できる
  • test_judge変更する必要がない

欠点

  • 2 桁以上 の数値座標の場合は、文字数を揃える必要があり、わかりづらくなる

数値座標を "," で区切って連結するデータ構造

下記のプログラムは、数値座標"," で区切って連結 した文字列で記述した場合のテストケースです。少し長く なりますが、数値座標の 区切りわかりやすく なります。

testcases = [
    # 〇 の勝利のテストケース
    [                      # 着手順とゲーム盤
        "0,3,1,4,2",       # 135  ooo
        Marubatsu.CIRCLE,  # 24.  xx. 
    ],                     # ...  ...
    # × の勝利のテストケース
    [                      # 着手順とゲーム盤
        "3,0,4,1,6,2",     # 246  xxx 
        Marubatsu.CROSS,   # 13.  oo.
    ],                     # 5..  ...
    # 引き分けのテストケース
    [                        # 着手順とゲーム盤
        "0,3,1,4,5,2,6,7,8", # 136  oox 
        Marubatsu.DRAW,      # 245  xxo
    ],                       # 789  oxo
]

"," で数値座標を区切った場合は、以前の記事同様の方法 で、下記のプログラムのように split メソッド を使って、数値座標要素 とする list に変換 することができます。

また、split メソッド を使った場合は、前回の記事 で説明したように、数値座標を 2 文字以上 で記述した場合でも正しく 動作 します。

def test_judge(testcases):
    for testcase in testcases:
        testdata, winner = testcase
        mb = Marubatsu()
        for coord in testdata.split(","):
            x, y = num_to_xy(coord)
            mb.move(x, y)
        print(mb)

        if mb.judge() == winner:
            print("ok")
        else:
            print("error!")
修正箇所
def test_judge(testcases):
    for testcase in testcases:
        testdata, winner = testcase
        mb = Marubatsu()
-       for coord in testdata:
+       for coord in testdata.split(","):
            x, y = num_to_xy(coord)
            mb.move(x, y)
        print(mb)

        if mb.judge() == winner:
            print("ok")
        else:
            print("error!")

下記のプログラムを実行することで、修正した test_judge が正しく動作することが確認できます。実行結果は先程と同様になるので省略します。

test_judge(testcases)

利点と欠点

上記の事から、このデータ構造の利点と欠点は以下のようになります。

利点

  • 2 桁以上 の数値座標の場合も 扱える
  • "," で区切るので、数値座標の 区切りわかりやすい

欠点

  • "," を記述する分だけ、記述が 少し長くなる

数値座標を使うべきかどうか

今回の記事の最初で説明した、数値座標利点と欠点 を再掲します。なお、先ほど説明した内容を、利点に追記 しています。

利点

  • 座標を 1 文字短く記述 できる
  • Excel 座標のように、アルファベットが含まれない ので、テーブル を用意する 必要がなく計算だけで xy 座標に 変換 できる

欠点

  • 慣れないと 座標の 意味分かりづらい

〇×ゲームのような、小さいゲーム盤 の場合は、慣れれば 欠点はあまり 問題にならない ので、この欠点が 気にならない人 は数値座標を 採用しても良い のではないかと思います。

また、2 次元の座標 を 1 つの 1 次元の数値座標 に変換して表現することは、実際よくある事 なので、数値座標の 考え方覚えて おいて 損はない と思います。

テストケースを記述 する際に 重要性質 は、短く記述できる ことと、間違わない ように 記述できる ことです。数値座標は、Excel 座標より短く記述できますが、慣れない間違った座標を記述 してしまう 可能性が高い ので、本記事では Excel 座標を採用する ことにします。

もちろん、数値座標でテストケースを表現してはいけないということは無いですし、様々なデータ構造 を実際に記述して 体験 するのは、プログラミングの 技術の向上 につながるので、余裕がある方は、数値座標 でテストケースを表すことにも チャレンジ してみて下さい。

着手する順番を表現するデータ構造

これまで紹介 した データ構造 は、いずれも 着手を行う 座標 を、着手する順番で 表記 するというものでしたが、それ以外 のデータ構造で 複数の着手 を表すことができます。その方法について少し考えてみて下さい。

本記事では、テストケースのコメントに、下記のように、それぞれのマスに 何番目着手を行う かを 表す文字列 を記述してきました。このデータ 使って、着手を行う ことができます。

    # 〇 の勝利のテストケース
    [                      # 着手順とゲーム盤
        "0,3,1,4,2",       # 135  ooo
        Marubatsu.CIRCLE,  # 24.  xx. 
    ],                     # ...  ...

着手順を直接連結するデータ構造

以下のようなデータ構造で、着手 を表す 複数のデータ記述 するという方法があります。

  • 先頭の文字 を、左上のマス対応 させ、1 文字ごと に、右のマス対応 させる。ただし、右端 のマスと、次の行の左端 のマスが つながっている ものとして考える
  • 文字 は、1 そのマスに 着手 を行う 1 から はじまる 順番 を表す 整数 を記述する
  • そのマスに 着手を行わない 場合は、. を記述 する

本記事では、このデータ構造の事を、着手順の文字列 と表記することにします。

上記の 〇 の勝利のテストケースの場合、着手順の文字列 をプログラムで 記述 すると以下のようになります。なお、このデータは、着手(move)の順番(order)を表すので、move_order という名前の 変数に代入 します。

move_order = "13524...."

行数が増えるので、本記事ではそのように記述しませんが、分かりにくいと思った方は、このデータを下記のプログラムのように、3 文字ごとに 改行して記述 することもできます。文の途中で改行 する際に、改行の直前\ を記述する 必要がある点に注意して下さい。

move_order = "135" + \
             "24." + \
             "..."

下記のプログラムは、着手順の文字列で記述した場合のテストケースです。着手順の文字列 では、着手の数 に関わらず、常に 9 文字文字列 でデータを 記述 します。

testcases = [
    # 〇 の勝利のテストケース
    [                      # 着手順とゲーム盤
        "13524....",       # 135  ooo
        Marubatsu.CIRCLE,  # 24.  xx. 
    ],                     # ...  ...
    # × の勝利のテストケース
    [                      # 着手順とゲーム盤
        "24613.5..",       # 246  xxx 
        Marubatsu.CROSS,   # 13.  oo.
    ],                     # 5..  ...
    # 引き分けのテストケース
    [                      # 着手順とゲーム盤
        "136245789",       # 136  oox 
        Marubatsu.DRAW,    # 245  xxo
    ],                     # 789  oxo
]

なお、着手順の文字列 のデータ構造は、複数数値座標直接連結 するデータ構造の場合と 同じ理由 で、マスの数2 桁以上 になるようなゲーム盤を表現する場合は 適していません

n 番目に着手を行う座標の計算

このデータ構造の、下記の 性質 から、左上マスの座標 は、文字列先頭0 番インデックスその右マスの座標 は、文字列1 番インデックス ・・・のように、マスの座標インデックスの番号 がそれぞれ 対応する ことがわかります。

  • 先頭の文字 を、左上のマス対応 させ、1 文字ごと に、右のマス対応 させる。ただし、右端 のマスと、次の行の左端 のマスが つながっている ものとして考える

この性質と、数値座標 の下記の 性質似ている と思いませんか?そのことに 気づけば着手順の文字列それぞれ の文字の インデックス が、数値座標対応する ことがわかります。

  • 左上のマス の座標を 0 とし、右方向 にマスが ずれる1 ずつ増える ように割り当てる。ただし、右端 のマスと、次の行の左端 のマスが つながっている ものとして考える

つまり、n 番目着手 する マス は、着手順の文字列 の中で、n が記述 されている 文字のインデックス計算 することで 知る ことができます。

index メソッドによる数値座標の計算

以前の記事 で説明したように、シーケンス型 のデータは、index というメソッドを使って、実引数に記述 した データ先頭の要素から探し最初に見つかった データの インデックスを計算 することができるので、先程の move_order から、3 番目に着手 を行う 数値座標 は、下記の 1 行目のプログラムで 計算する ことができます。3 行目で num_to_xy数値座標xy 座標変換 することで、実行結果から、3 番目の着手(1, 0) のマス に行うという、正しい計算が行われていることが確認できます。

coord = move_order.index("3")
print(coord)
print(num_to_xy(coord))

実行結果

1
(1, 0)

〇×ゲームのゲーム盤の マスの数9 マス なので、1 から 9 までの 繰り返し処理 を行うことで、上記の方法で、1 手目から 9 手目まで着手 を行う 座標順番に計算 することができます。従って、下記のプログラムで move_order の順番着手を行う ことができます。

下記のプログラムで行われる処理は、以下の通りです。

  • 1 行目:新しい Marubatsu クラスのインスタンスを作成する
  • 2 行目:for 文で 1 から 9 まで の繰り返しを行う。この場合は、range(9) ではなく、range(1, 10)1 のように記述する点に注意が必要である
  • 3 行目i 番の着手 を表す 数値座標計算 する
  • 4 行目数値座標 から x 座標y 座標計算 する
  • 5 行目(x, y) のマスに 着手 を行う
  • 6 行目:正しいマスに着手が出来たかどうかを 確認 できるように、ゲーム盤を表示 する

range(9) のように記述したい場合は、i の値1 つ小さくなる ので、3 行目を coord = move_order.index(i + 1) のように修正して 調整 します。個人的には どちらでも良い と思いますので、分かりやすい思ったほう採用 して下さい。

1  mb = Marubatsu()
2  for i in range(1, 10):
3      coord = move_order.index(i)
4      x, y = num_to_xy(coord)
5      mb.move(x, y)
7      print(mb)
行番号のないプログラム
mb = Marubatsu()
for i in range(9):
    coord = move_order.index(i + 1)
    x, y = num_to_xy(coord)
    mb.move(x, y)
    print(mb)

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\030\marubatsu.ipynb セル 31 line 3
      1 mb = Marubatsu()
      2 for i in range(9):
----> 3     coord = move_order.index(i + 1)
      4     x, y = num_to_xy(coord)
      5     mb.move(x, y)

TypeError: must be str, not int

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

  • TypeError
    データ型(type)に関するエラー
  • must be str, not int
    整数型(int)ではなく(not)、文字列型(str)でなければならない(must be)

プログラムを実行すると、上記の実行結果のような エラーが発生 します。エラーメッセージ から、何か整数型ではなく文字列型でなければならない ことがわかります。3 行目の中で、move_order には文字列型のデータが代入されていますが、i には 整数型 のデータが 代入 されているので、このことが 原因 であることが 推測 されます。

文字列型 のデータである move_order に対して、index メソッド文字列を探す 場合は、index メソッドの 実引数文字列型 である 必要 があり、そのことが エラーの原因 です。従って、数値型i文字列型 のデータに 変換 する必要があります。

組み込み関数 str

Python では、組み込み関数 str を利用することで、実引数 に記述したデータを、文字列型 のデータに 変換 することができます。

厳密には、str組み込みクラス で、str を呼び出すことで、実引数に記述したデータを表す、文字列型インスタンスを作成 します。これは、int も同様 です。

str に関する詳細は、下記のリンク先を参照して下さい。

従って、下記のプログラムのように、先程のプログラムの index メソッドの 実引数str(i) に修正 することで、このエラーが発生しないようになりますが、別のエラーが発生 してしまいます。その原因について少し考えてみて下さい。

mb = Marubatsu()
for i in range(1, 10):
    coord = move_order.index(str(i))
    x, y = num_to_xy(coord)
    mb.move(x, y)
    print(mb)
修正箇所
mb = Marubatsu()
for i in range(1, 10):
-   coord = move_order.index(i)
+   coord = move_order.index(str(i))
    x, y = num_to_xy(coord)
    mb.move(x, y)
    print(mb)

実行結果

Turn x
o..
...
...

略

Turn x
ooo
xx.
...

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\030\marubatsu.ipynb セル 32 line 3
      1 mb = Marubatsu()
      2 for i in range(1, 10):
----> 3     coord = move_order.index(str(i))
      4     x, y = num_to_xy(coord)
      5     mb.move(x, y)

ValueError: substring not found

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

  • ValueError
    値(value)に関するエラー
  • substring not found
    substring([] の中に記述するインデックスの事)が見つからなかった(not found)

上記の実行結果から、5 手目まで正しく動作 することが確認できますが、その 次の着手エラーが発生 しています。このエラーは、以前の記事で説明したように、index メソッドに記述した 実引数 が、シーケンス型の 要素の中見つからなかった ことを表します。エラーが発生した時点で 5 つの着手行われている ので、str(i) は次の "6" になります が、実際に、move_order に代入されている "13524...." の中に "6" という文字列は 存在しません

このエラーを発生させないようにするためには、move_order の中 に、str(i)存在するかどうかチェック する必要があります。また、存在しない場合 は、それ 以降の着手行う必要がない ので for 文による 繰り返し処理 をその時点で 終了する 必要があります。

in 演算子と not in 演算子

シーケンス型 のデータの 要素 に、特定のデータ存在するかどうか を、in 演算子調べる ことができます。in 演算子は、下記のように記述し、シーケンス型のデータの要素にデータが 存在する 場合は True存在しない 場合は False計算 されます。

データ in シーケンス型

シーケンス型のデータの要素に、特定のデータが 存在しないこと を、not in 演算子 で、下記のように記述することで 調べる ことができます。計算結果は 同様TrueFalse です。

データ not in シーケンス型

innot in 演算子は、dict などの マッピング型 に対しても 利用できます

シーケンス型に対する in 演算子の詳細については、下記のリンク先を参照して下さい。

break 文と continue

for 文などによる 繰り返し処理ブロックの中 で、break を記述 することで、繰り返し処理終了 することができます。break1 つの処理 を表すので、break 文 と呼びます。

下記のプログラムは、for 文で 0 から 9 まで繰り返し処理 を行いますが、for 文の ブロックの中if 文 で、i5 になった場合break 文を実行 するので、実行結果には 0 から 4 まで の数字しか表示されません。

for i in range(10):
    if i == 5:
        break
    print(i)

実行結果

0
1
2
3
4

今回の記事では使用しませんが、break 文と 類似 する文に continue があります。

for 文などによる 繰り返し処理ブロックの中 で、continue を記述 することで、残り の繰り返し処理の ブロックを実行せず に、次の繰り返し処理を始める ことができます。

下記のプログラムは、for 文で 0 から 9 まで繰り返し処理 を行いますが、for 文の ブロックの中if 文 で、i を 2 で割った余りが 0、すなわち i が 偶数 の場合に continue 文を実行 するので、実行結果には 0 から 9 までの 奇数のみが表示 されます。

for i in range(10):
    if i % 2 == 0:
        continue
    print(i)

実行結果

1
3
5
7
9

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

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

not in 演算子と break 文を使った修正

not in 演算子break 文 を使って、先程のプログラムを以下のように修正します。

3 行目で、not in 演算子 を使って、str(i)move_order の中に存在しない ことをチェックし、存在しない場合は、4 行目の break 文 で、繰り返しを終了 します。

実行結果から、正しい順番で 5 つの着手が行われることが確認できます。

1  mb = Marubatsu()
2  for i in range(1, 10):
3      if str(i) not in move_order:
4          break
5      coord = move_order.index(str(i))
6      x, y = num_to_xy(coord)
7      mb.move(x, y)
8      print(mb)
行番号のないプログラム
mb = Marubatsu()
for i in range(1, 10):
    if str(i) not in move_order:
        break
    coord = move_order.index(str(i))
    x, y = num_to_xy(coord)
    mb.move(x, y)
    print(mb)
修正箇所
mb = Marubatsu()
for i in range(1, 10):
+   if str(i) not in move_order:
+       break
    coord = move_order.index(str(i))
    x, y = num_to_xy(coord)
    mb.move(x, y)
    print(mb)

実行結果

Turn x
o..
...
...

Turn o
o..
x..
...

Turn x
oo.
x..
...

Turn o
oo.
xx.
...

Turn x
ooo
xx.
...

test_judge の修正

test_judge は以下のように修正します。修正内容は以下の通りです。

  • 3 行目:変数の名前を先程のプログラムに合わせて testdata から move_order に修正する2
  • 5 ~ 7 行目:元の for 文を削除 し、先ほど記述 したプログラムを ここに記述 する
 1  def test_judge(testcases):
 2      for testcase in testcases:
 3          move_order, winner = testcase
 4          mb = Marubatsu()
 5          for i in range(1, 10):
 6              if str(i) not in move_order:
 7                  break
 8              coord = move_order.index(str(i))
 9              x, y = num_to_xy(coord)
10              mb.move(x, y)
11          print(mb)
12
13          if mb.judge() == winner:
14              print("ok")
15          else:
16              print("error!")
行番号のないプログラム
def test_judge(testcases):
    for testcase in testcases:
        move_order, winner = testcase
        mb = Marubatsu()
        for i in range(1, 10):
            if str(i) not in move_order:
                break
            coord = move_order.index(str(i))
            x, y = num_to_xy(coord)
            mb.move(x, y)
        print(mb)
 
        if mb.judge() == winner:
            print("ok")
        else:
            print("error!")
修正箇所
def test_judge(testcases):
    for testcase in testcases:
-       testdata, winner = testcase
+       move_order, winner = testcase
        mb = Marubatsu()
-       for coord in testdata.split(","):
+       for i in range(1, 10):
+           if str(i) not in move_order:
+               break
+           coord = move_order.index(str(i))
            x, y = num_to_xy(coord)
            mb.move(x, y)
        print(mb)

        if mb.judge() == winner:
            print("ok")
        else:
            print("error!")

下記のプログラムを実行することで、修正した test_judge が正しく動作することが確認できます。実行結果は先程と同様になるので省略します。

test_judge(testcases)

利点と欠点

着手順の文字列のデータ構造の利点と欠点は以下の通りです。

利点

  • 必ず 9 文字短く記述 できる

欠点

  • 途中で改行 するという 工夫 をしないと、分かりづらい
  • 着手の数少ない場合 でも、9 文字を記述 する 必要 がある

実際には、着手順の文字列の 末尾の "."省略 することができます。例えば、"13524....""13524" のように 省略して記述 することができます。

その理由は以下の通りです。

  • 末尾の "." を削除しても、文字列の中で 数字が記述 された 位置が変化しない
  • 処理の中で、index メソッドで、"." の位置を 検索しない

数字の位置が変化 してしまうので、数字より前 に記述された "."削除できません

なお、末尾の "." を削除してしまうと、3 文字ごとに改行 を行うという 工夫が出来なくなってしまう ので、見た目わかりづらくなる という 欠点 が生じます。

着手順を "," で区切って連結するデータ構造

表記が長くなる という 欠点 がありますが、これまでに紹介 してきた場合と 同様 に、着手順"," で区切って連結 するという データ構造 で表現することもできます。下記のプログラムは、そのように記述した場合のテストケースです。この場合は、必ず ","8 つ記述する 必要があるので、常に 先ほどの ほぼ倍17 文字を記述 する必要があります。

testcases = [
    # 〇 の勝利のテストケース
    [                        # 着手順とゲーム盤
        "1,3,5,2,4,.,.,.,.", # 135  ooo
        Marubatsu.CIRCLE,    # 24.  xx. 
    ],                       # ...  ...
    # × の勝利のテストケース
    [                        # 着手順とゲーム盤
        "2,4,6,1,3,.,5,.,.", # 246  xxx 
        Marubatsu.CROSS,     # 13.  oo.
    ],                       # 5..  ...
    # 引き分けのテストケース
    [                        # 着手順とゲーム盤
        "1,3,6,2,4,5,7,8,9", # 136  oox 
        Marubatsu.DRAW,      # 245  xxo
    ],                       # 789  oxo
]

test_judge の修正

着手順を "," で区切って連結するデータ構造では、着手順 を表す文字の インデックス と、数値座標対応しなく なりますが、データを、split メソッドによって list に変換 することで、着手順 を表すデータが 代入 された 要素インデックス と、数値座標再び対応する ようになります。従って、test_judge の修正は、下記のプログラムのように、4 行目に その処理を追加する修正 を行う だけで済みます

 1  def test_judge(testcases):
 2      for testcase in testcases:
 3          move_order, winner = testcase
 4          move_order = move_order.split(",")
 5          mb = Marubatsu()
 6          for i in range(1, 10):
 7              if str(i) not in move_order:
 8                  break
 9              coord = move_order.index(str(i))
10              x, y = num_to_xy(coord)
11              mb.move(x, y)
12          print(mb)
13
14          if mb.judge() == winner:
15              print("ok")
16          else:
17              print("error!")
行番号のないプログラム
def test_judge(testcases):
    for testcase in testcases:
        move_order, winner = testcase
        move_order = move_order.split(",")
        mb = Marubatsu()
        for i in range(1, 10):
            if str(i) not in move_order:
                break
            coord = move_order.index(str(i))
            x, y = num_to_xy(coord)
            mb.move(x, y)
        print(mb)
 
        if mb.judge() == winner:
            print("ok")
        else:
            print("error!")
修正箇所
def test_judge(testcases):
    for testcase in testcases:
        move_order, winner = testcase
+       move_order = move_order.split(",")
        mb = Marubatsu()
        for i in range(1, 10):
            if str(i) not in move_order:
                break
            coord = move_order.index(str(i))
            x, y = num_to_xy(coord)
            mb.move(x, y)
        print(mb)
 
        if mb.judge() == winner:
            print("ok")
        else:
            print("error!")

下記のプログラムを実行することで、修正した test_judge が正しく動作することが確認できます。実行結果は先程と同様になるので省略します。

test_judge(testcases)

利点と欠点

着手順を表す文字列を "," で区切って連結するデータ構造の利点と欠点は以下の通りです。

利点

  • 2 桁以上 の着手順を 表現できる

欠点

  • 着手順の文字列 と比べて 約 2 倍以上長さ になる
  • 途中で改行 するという 工夫 をしないと、意味が分かりづらい

着手順を list で表現するデータ構造

着手順 を最初から list で表記 するという方法も考えられます。下記のプログラムは、そのように記述した場合のテストケースです。

testcases = [
    # 〇 の勝利のテストケース
    [                        # 着手順とゲーム盤
        [1,3,5,2,4,0,0,0,0], # 135  ooo
        Marubatsu.CIRCLE,    # 24.  xx. 
    ],                       # ...  ...
    # × の勝利のテストケース
    [                        # 着手順とゲーム盤
        [2,4,6,1,3,0,5,0,0], # 246  xxx 
        Marubatsu.CROSS,     # 13.  oo.
    ],                       # 5..  ...
    # 引き分けのテストケース
    [                        # 着手順とゲーム盤
        [1,3,6,2,4,5,7,8,9], # 136  oox 
        Marubatsu.DRAW,      # 245  xxo
    ],                       # 789  oxo
]

list の要素["1","3","5","2","4",".",".",".","."] のように、文字列で記述 することもできますが、そうすると すべての要素の前後" を記述する 必要があるため かなり長く なってしまいます。また、list の要素整数型 のデータで記述することで、test_judge の中で、move_order の要素の中から 着手順 を表すデータを index メソッドで 検索 する際に、coord = move_order.index(str(i)) のように、i文字列型に変換 する 必要なくなります。従って、list の要素整数型で記述するべき です。

今回の記事の最初の方で説明したように、座標を表す データ型の種類統一するべき です。これまでは、着手を行わないマス に対応する データ"." という 文字列型 のデータで記述していたので、このデータを 数値型 のデータで記述するように 修正する 必要があります。そこで、本記事では、"." という文字列を、0 という 整数に変更 しています。

test_judge の修正

下記は、test_judge を修正したプログラムです。

着手順を list で表現 する データ構造 では、最初から データが list で記述 されているので、split メソッドで文字列型のデータを list に変換 する 必要がなくなります

list の要素 には、最初から数値型 のデータが 代入 されているので、6 行目と 8 行目で 行っていた str を使って文字列型のデータを数値型に 変換する必要ありません

 1  def test_judge(testcases):
 2      for testcase in testcases:
 3          move_order, winner = testcase
 4          mb = Marubatsu()
 5          for i in range(1, 10):
 6              if i not in move_order:
 7                  break
 8              coord = move_order.index(i)
 9              x, y = num_to_xy(coord)
10              mb.move(x, y)
11          print(mb)
12 
13          if mb.judge() == winner:
14              print("ok")
15          else:
16              print("error!")
行番号のないプログラム
def test_judge(testcases):
    for testcase in testcases:
        move_order, winner = testcase
        mb = Marubatsu()
        for i in range(1, 10):
            if i not in move_order:
                break
            coord = move_order.index(i)
            x, y = num_to_xy(coord)
            mb.move(x, y)
        print(mb)
 
        if mb.judge() == winner:
            print("ok")
        else:
            print("error!")
修正箇所
def test_judge(testcases):
    for testcase in testcases:
        move_order, winner = testcase
-       move_order = move_order.split(",")
        mb = Marubatsu()
        for i in range(1, 10):
-           if str(i) not in move_order:
+           if i not in move_order:
                break
-           coord = move_order.index(str(i))
+           coord = move_order.index(i)
            x, y = num_to_xy(coord)
            mb.move(x, y)
        print(mb)
 
        if mb.judge() == winner:
            print("ok")
        else:
            print("error!")

下記のプログラムを実行することで、修正した test_judge が正しく動作することが確認できます。実行結果は先程と同様になるので省略します。

test_judge(testcases)

利点と欠点

着手順を list で表現するデータ構造の利点と欠点は以下の通りです。

利点

  • 2 桁以上 の着手順を 表現できる
  • 最初から list と数値型のデータで記述するので、処理が行いやすい

欠点

  • 着手順の文字列 と比べて 約 2 倍以上 の長さになる
  • 途中で改行 するという 工夫 をしないと、意味が分かりづらい

着手順の 3 つのデータ構造の比較

〇の勝利のテストケースのデータを、3 つのデータ構造で並べて記述すると以下のようになります。着手順の文字列最も短く記述できる ので、〇×ゲームのように、1 文字着手順を記述できる 場合は、着手順の文字列有力な候補 になると思います。

一方、"," で区切って並べる場合と、list で表現する場合の 記述の長さ全く同じ ですが、list で表現 するほうが プログラムで扱いやすい ので、着手順を "," で区切って連結するデータ構造を使う 理由はあまりない と思います。

"13524...."           # 着手順の文字列
"1,3,5,2,4,.,.,.,."   # 着手順を "," で区切って連結する
[1,3,5,2,4,0,0,0,0]   # 着手順を list で表現する

今回の記事のまとめ

今回の記事では、数値座標 と、着手順データ構造 とアルゴリズムを紹介しました。

数値座標 は、Excel 座標や xy 文字座標と 比較 して、さらに短く記述 できるという利点がありますが、分かりやすさ の点では、Excel 座標や xy 座標に 劣る感じる人が多い のではないかと思います。ただし、〇×ゲームのような 小さなゲーム盤 の場合の場合は、慣れれば十分実用的 だと思いますので、こちらのほうが 良いと思った方 は、ぜひ 採用してみて下さい

着手順を表現 する データ構造 は、これまでとは 全く異なる観点 で着手を表すデータを 表現 します。こちらは、着手の数に関わらず、常に一定数 のデータを 記述 しなければならないという 欠点 がありますが、着手の数が多い 場合は、Excel 座標や xy 文字座標と比較して 短く記述できる という利点があります。また、着手の数1 桁 の場合は、人によって はこちらのほうが 見た目が分かりやすい 場合もあるのではないかと思います。

本記事 では Excel 座標"," で区切って連結 する方法を 採用 しますが、本記事で紹介したデータ構造の中で 気に入ったもの があれば それを採用して下さい。また、本記事で紹介したよりも 効率的なデータ構造思いついた場合 は、もちろん そちらを採用 して下さい。

次回の記事では、今回までの知識を利用 して、中断していた テストの作業を再開 します。

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

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

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

以下のリンクは、今回の記事で作成した test.py です。データ構造ごとに名前を変えてそれぞれの関数を定義しています。また、docstring などは省略します。

次回の記事

  1. range(1, 10) の意味については、以前の記事を参照して下さい

  2. 名前を testdata のままにしたい場合は、6 行目と 8 行目の move_ordertestdata に修正して下さい

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