LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その29 文字列で表現する xy 座標

Last updated at Posted at 2023-11-19

目次と前回の記事

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

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

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

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

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

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

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

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

前回までのおさらい

前回の記事では、複数 の Excel 座標を 文字列で表現 する データ構造 と、その文字列を list に変換 する アルゴリズム を紹介しました。

前回までの記事で、座標を表すデータ構造として、xy 座標と Excel 座標を紹介しました。今回の記事では それ以外 の座標を表す データ構造 として、xy 文字座標 を紹介します。

xy 文字座標

Excel 座標データ構造 は、アルファベット1 から始まる整数 とし、その 2 つを並べた文字列 で表現するというものでした。それに対して、整数 で表現し、その 2 つを並べた文字列 で座標を表現するという データ構造 が考えられます。

このデータ構造は、list で表現する xy 座標 を、そのまま文字列で表現 するのものなので、本記事ではこのデータ構造の事を xy 文字座標 と表現することにします。

xy 座標を そのまま 文字列で表現するので、座標を 1 から数える Excel 座標と異なり、xy 文字座標では、座標を 0 から数える 点に注意して下さい。

本記事では採用しませんが、Excel 座標のように、座標を 1 から数えるという方法も考えられます。自分がわかりやすいと思ったほう採用 してもかまいません。

xy 文字座標を、どのような文字列で表現するかについては、いくつかの方法が考えられ、それぞれ利点と欠点があります。

xy 文字座標のデータ構造その 1(直接並べる)

最も短く表記 できる方法として、Excel 座標のように、x 座標と y 座標を表す文字列を 直接並べて記述 するというデータ構造が考えられます。例えば、(0, 1) のマスの座標を "01" のように表記します。

利点と欠点

このデータ構造の利点と欠点は以下の通りです。

利点

  • xy 文字座標の中で、最も短く 表記できる
  • xy 座標に データを変換 する際に、テーブルが不要 になる

欠点

  • x 座標と y 座標の 区切りわかりづらい
  • 一見する と、2 桁の数値 を表すデータのように 見えてしまう
  • x 座標と y 座標の 桁数を決めておかないと x 座標と y 座標の 区別がつかない 場合がある

2 番目の利点については、xy 文字座標を xy 座標に変換するプログラムの所で説明します。

このデータ構造を利用する際には、3 番目の欠点正しく理解 しておくことが重要です。〇× ゲームの場合は x 座標も y 座標も 1 桁の整数で表現できるので問題ありませんが、囲碁のような 2 桁の座標 があるゲーム盤の場合、x 座標と y 座標の 桁数を決めておかないと、例えば "110" という xy 文字座標が、(1, 10) を表すのか、(10, 1) を表すのかの 区別が付けられない ことになってしまいます。囲碁の場合は、x 座標と y 座標の桁数を 必ず 2 桁で表現する ことにすれば、(1, 10) は、 "0110"、(10, 1) は "1001" のように 区別できるように なりますが、あまり わかりやすいとは言えないでしょう

xytext_to_xy の定義

xy 文字座標を利用するために、以前の記事 で、Excel 座標を xy 座標に変換する関数を定義 した場合と同様に、xy 文字座標を xy 座標に変換 する、xytext_to_xy という名前の 関数を定義 することにします。

xttext_to_xy は、以前の記事 で紹介した、下記の excel_to_xy の定義その 4 を ほぼそのまま流用 することで、定義することが出来ます。

1  def excel_to_xy(coord):
2      excel_to_x_table = {
3          "A": 0,
4          "B": 1,
5          "C": 2,
6      }
7      x, y = coord
8      return excel_to_x_table[x], int(y) - 1
行番号のないプログラム
def excel_to_xy(coord):
    excel_to_x_table = {
        "A": 0,
        "B": 1,
        "C": 2,
    }
    x, y = coord
    return excel_to_x_table[x], int(y) - 1

上記のプログラムは、7 行目で、Excel 座標の x 座標と y 座標 を表す文字列を x, y に代入 しています。また、8 行目で int(y) - 1 を記述することで、y 座標を計算 しています。

xy 文字座標も、同様の方法 で xy 文字座標の x 座標と y 座標 を表す文字列を x, y に代入 することができます。xy 文字座標は、Excel 座標と異なり、座標を 0 から数える ので、y 座標 は、int(y) で計算 することができます。また、xy 文字座標は、x 座標 も y 座標と 同様整数を表す文字列 で表現されているので、x 座標int(x) で計算 することができます。従って、xytext_to_xy は、下記のプログラムのように定義できます。

def xytext_to_xy(coord):
    x, y = coord
    return int(x), int(y)
修正箇所
- def excel_to_xy(coord):
+ def xytext_to_xy(coord):
-   excel_to_x_table = {
-       "A": 0,
-       "B": 1,
-       "C": 2,
-   }
    x, y = coord
-   return excel_to_x_table[x], int(y) - 1
+   return int(x), int(y)

xy 文字座標利点 の一つに、excel_to_xy の場合と異なり、アルファベットを数値に変換するための テーブルが不要になる という性質があります。

上記の xytext_to_xy は、〇× ゲームのような、x 座標と y 座標が 1 文字で表現 される 場合でのみ 利用できる点に注意が必要です。

複数の xy 文字座標を表すデータ構造

次に、複数 の xy 文字座標を まとめて扱う データ構造を考えることにします。xy 文字座標は、Excel 座標と同様に 文字列で表現 されるので、前回の記事で説明したように、座標を直接連結する 方法と、"," などの 文字列で区切って連結 する方法が考えられます。

この 2 つのデータ構造の 利点と欠点 については、前回の記事の こちらこちら を参照して下さい。今回の記事では、そのことを踏まえた上でのそれぞれの性質を説明します。

xy 文字座標を直接連結するデータ構造

下記は、xy 文字座標を 直接連結 するデータ構造でテストケースを記述するプログラムです。下記のように、xy 文字座標は すべて数字で表現 されるので、xy 文字座標の 区切り がどこにあるのかが 非常に分かりづらく なっています。そのため、筆者は下記のプログラムをかなり わかりづらいと思いながら 記述しました。また、このデータ構造を利用すると、データの 入力ミスが多発 すると思いますので、個人的には お勧めしません実際に 筆者は下記のテストケースを入力する際に 一部のデータを間違えました

from marubatsu import Marubatsu

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

このデータ構造を、xy 文字座標を要素とする list に変換 する処理は、前回の記事excels_to_list全く同じ です。そのため、このデータ構造を list に変換する処理を、以下の いずれかの方法 で行うことが出来ます。

  1. excels_to_list を、名前を変えずに そのまま利用 する
  2. xytext_to_list という名前で、excels_to_list同じ処理 を行う 関数を定義 する
  3. 文字列を list に変換 する処理を行うので、excels_to_listtext_to_list という 名前に変更 して利用する。この場合は、前回のプログラムも修正 する必要がある
  4. 名前だけ が気になる場合は、下記のプログラムのように、xytext_to_listexcels_to_list代入 する
xytext_to_list = excels_to_list

上記の中の 2 番目の、全く同じ処理 を行う 関数複数定義 する方法は、その関数の処理を 修正する必要 が生じた場合に、両方を修正する必要 があるため、お勧めしません。4 番目の方法であればそのような欠点は生じませんが、excel_to_list の定義を修正 した場合は、xytext_to_list = excels_to_list実行しなおさない と、その修正が xytext_to_list反映されない ので、4 番目の方法もあまり 良い方法ではありません

そこで、本記事では、3 番目の方法を取ることにします。なお、関数の 名前だけを修正 すると、関数の 仮引数excels という 名前が変 になってしまうので、仮引数の名前ローカル変数の名前excelscoords に、下記のプログラムのように 修正 します。

def text_to_list(coords):
    coord_list = [ coords[i:i+2] for i in range(0, len(coords), 2) ]
    return coord_list
修正箇所
- def excels_to_list(excels):
+ def text_to_list(coords):
-   excel_list = [ excels[i:i+2] for i in range(0, len(excels), 2) ]
+   coord_list = [ coords[i:i+2] for i in range(0, len(coords), 2) ]
-   return excel_list
+   return coord_list

データ構造が変わったので、前回の記事 と同様の方法で、test_judge を修正します。修正箇所は、Excel 座標に関連 する excels_to_listexcel_to_xy2 箇所 です。

def test_judge(testcases):
    for testcase in testcases:
        testdata, winner = testcase
        mb = Marubatsu()
        for coord in text_to_list(testdata):
            x, y = xytext_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 excels_to_list(testdata):
+       for coord in text_to_list(testdata):
-           x, y = excel_to_xy(coord)            
+           x, y = xytext_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
ooo
xx.
...

ok
Turn o
xxx
oo.
o..

ok
Turn x
oox
xxo
oxo

ok

xy 文字座標を "," で区切って連結するデータ構造

下記は、xy 文字座標を "," で区切って連結 するデータ構造でテストケースを記述するプログラムです。こちらは、先程よりかなり わかりやすく記述 できます。xy 文字座標 の入力に 違和感を感じない人 は、このデータ構造を 利用しても良い と思います。

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

データ構造が変わったので、前回の記事 と同様の方法で、test_judge を修正します。修正箇所は、text_to_list(testdata) で、testdata.split(",") に修正します。

def test_judge(testcases):
    for testcase in testcases:
        testdata, winner = testcase
        mb = Marubatsu()
        for coord in testdata.split(","):
            x, y = xytext_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 text_to_list(testdata):
+       for coord in testdata.split(","):
            x, y = xytext_to_xy(coord)            
            mb.move(x, y)
        print(mb)

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

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

test_judge(testcases)

xy 文字座標のデータ構造その 2("," で区切る)

先程の、x 座標と y 座標を 直接並べて記述 するデータ構造は、x 座標と y 座標の 区別がつきづらい などの欠点がありました。そこで、x 座標と y 座標を "," などの 文字列で区切って"0,1" のように記述するというデータ構造が考えられます。

利点と欠点

このデータ構造の利点と欠点は以下の通りです。

利点

  • x 座標と y 座標の 区切りわかりやすい
  • x 座標と y 座標に、任意の桁数 の数字を記述できる

欠点

  • 記述"," 等の区切りの文字の分だけ 長くなる
  • 複数の xy 座標を 連結して記述 する際に、工夫が必要 になる

2 番目の x 座標と y 座標の 桁数に制限がなくなる という利点は、囲碁のような 大きなゲーム盤座標を記述 する際には 大きな利点 となります。

一方で、このデータ構造では、2 番目の欠点が 新しく生じて しまいます。2 番目の欠点については、複数の xy 文字座標を表すデータ構造の所で説明します。

xytext_to_xy の定義

x 座標と y 座標を "," などの文字列で区切るというデータ構造は、複数Excel座標"," などの 文字列で区切る データ構造と 同じ なので、前回の記事 で紹介した、split メソッド を使って xytext_to_xy を下記のプログラムのように定義できます。

def xytext_to_xy(coord):
    x, y = coord.split(",")
    return int(x), int(y)
修正箇所
def xytext_to_xy(coord):
-   x, y = coord
+   x, y = coord.split(",")
    return int(x), int(y)

split メソッド文字列list に変換 した場合は、先程の x, y = coord の場合と異なり、xy には、任意の文字数 のデータが代入されます。また、int任意の文字数 の文字列を 整数に変換 できるので、このデータ構造は、下記のプログラムの実行結果からわかるように、任意の桁数 の x 座標と y 座標を 記述 することができます。

print(xytext_to_xy("10,200"))

実行結果

(10, 200)

split メソッド文字列list に変換 した場合は、数値の前後空白文字1を記述 することが できる という利点も得られます。その理由は、組み込み関数 int が、先頭末尾空白文字を無視 するからです。下記は、先頭と末尾に半角の 空白文字が 5 つ 記述された 文字列 を、整数型 のデータに 変換 するプログラムです。

print(int("     100     "))

実行結果

100

従って、下記のプログラムのように、中に 空白文字が記述 された、"10 , 20"xytext_to_xyxy 座標に変換 することが出来ます。

print(xytext_to_xy("10 , 20"))

実行結果

(10, 20)

なお、split メソッドを 使わない場合 は、xy 文字座標の中に、空白文字を記述 することは できない 点に注意して下さい。

xy 文字座標を直接連結するデータ構造

先程と同様 に複数の xy 文字座標をまとめて扱うデータ構造として、xy 文字座標を直接連結する方法と、"," 等の文字列で区切って連結する方法を考えることにします。

下記は、xy 文字座標を 直接連結 するデータ構造でテストケースを記述するプログラムです。見ればわかると思いますが、このデータ構造は 非常に分かりづらい ので、使わないほうが良い と思います。例えば、"0,00,11,01,12,0" は、(0, 0)、(0, 1)、(1, 0)、(1, 1)、(2, 0) の 5 つの座標 を表していますが、このデータ構造の事を知らない人は、00011011206 つのデータが記述 されている ようにしか見えない と思います。

また、このことは、xy 文字座標を区切る文字列を 別の文字列に変えて改善 することは ありません。例えば、区切る文字列を ":" にすると "0:00:11:01:12:0" のようになりますが、上記の欠点は全く 改善されません

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

参考まで に、この方法を採用する場合の text_to_list の修正方法 について説明します。〇×ゲームの xy 文字座標は、必ず 3 文字 で表現されるので、元の 2 文字ではなく、3 文字ずつ データを取り出したものを list の要素にするという修正を行います。具体的には、下記のプログラムのように、元の text_to_list のブロック内の 23 に修正します。

def text_to_list(coords):
    coord_list = [ coords[i:i+3] for i in range(0, len(coords), 3) ]
    return coord_list
修正箇所
def text_to_list(coords):
-   coord_list = [ coords[i:i+2] for i in range(0, len(coords), 2) ]
+   coord_list = [ coords[i:i+3] for i in range(0, len(coords), 3) ]
    return coord_list

test_judge を下記のように、xy 文字座標を 直接連結 する場合の 処理に戻してからtest_judge を実行することで、修正したデータ構造で、test_judge が正しく動作することが確認できます。実行結果は、先ほどと同じなので省略します。

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

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

test_judge(testcases)

xy 文字座標を "," で区切って連結するデータ構造

下記は、xy 文字座標を "," で区切って連結 するデータ構造でテストケースを記述するプログラムです。見ればわかると思いますが、x 座標と y 座標区切る文字列 と、xy 文字座標区切る文字列 に、同じ "," を記述 しているので、どの ","何を区切っているか非常にわかりづらく なっています。従ってこのデータ構造も、お勧めできません

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

参考までに、この方法を採用する場合の text_to_list の修正方法 について説明します。","2 つ異なる意味区切り記号 として使っているので、split使うだけ ではうまく text_to_list を実装することは できません

一つの方法として、splitlist に変換 したデータを、2 つずつ 集めて xy 座標を作成 し、それらを要素 とする list を作成する という方法が考えられます。下記のプログラムは、xy 文字座標 ではなくxy 座標要素 として持つ list を作成 する関数なので、text_to_list を修正するのではなく、新しく text_to_xylist という名前の 関数を定義 しています。

1  def text_to_xylist(coords):
2      coord_list = coords.split(",")
3      xy_list = []
4      for i in range(0, len(coord_list), 2):
5          x, y = coord_list[i:i+2]
6          xy_list.append([int(x), int(y)])
7      return xy_list
行番号のないプログラム
def text_to_xylist(coords):
    coord_list = coords.split(",")
    xy_list = []
    for i in range(0, len(coord_list), 2):
        x, y = coord_list[i:i+2]
        xy_list.append([int(x), int(y)])
    return xy_list

上記のプログラムで行われる処理は以下のようになります。

  • 2 行目"," で区切られた文字列を要素とする list を coord_list に代入する
  • 4 行目coord_list の要素には先頭から順番に、x 座標、y 座標 の順番で文字列のデータが代入されているので、先頭から 2 つずつ要素を取り出すことで、1 つの xy 座標を表すデータを得ることができる。4 行目は、そのような繰り返しを行う for 文である
  • 5 行目:スライス表記を使って、x 座標と y 座標を表す文字列を xy に代入する
  • 6 行目int を使ってそれぞれを整数型のデータに変換したものを要素とする xy 座標を表す list を xy_list に追加する
  • 7 行目:xy 座標を要素として持つ list を返り値として返る

text_to_xylist は、xy 座標 を要素として持つ list を返す 関数なので、5 行目の for 文に記述する変数に、直接 x, y を記述 することができ、その次の行にあった x, y = xytext_to_xy(coord) を削除 することができます。

 1  def test_judge(testcases):
 2      for testcase in testcases:
 3          testdata, winner = testcase
 4          mb = Marubatsu()
 5          for x, y in text_to_xylist(testdata):
 6              mb.move(x, y)
 7          print(mb)
 8
 9          if mb.judge() == winner:
10              print("ok")
11          else:
12              print("error!")
行番号のないプログラム
def test_judge(testcases):
    for testcase in testcases:
        testdata, winner = testcase
        mb = Marubatsu()
        for x, y in text_to_xylist(testdata):
            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 text_to_list(testdata):
+       for x, y in text_to_xylist(testdata):
-           x, y = xytext_to_xy(coord)            
            mb.move(x, y)
        print(mb)

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

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

test_judge(testcases)

具体例は紹介しませんが、正規表現を使う方法など、他にも様々な方法があります。

text_to_xylist を、リスト内包表記を使って、下記のように記述することができますが、見た目がわかりづらくなる ので、本記事では採用しないことにします。なお、リスト内包表現 は、3、4 行目のように 途中で改行を行う ことができます。

def text_to_xylist(coords):
    coord_list = coords.split(",")
    xy_list = [[int(coord_list[i]), int(coord_list[i+1])]
               for i in range(0, len(coord_list), 2)]
    return xy_list
修正箇所
def text_to_xylist(coords):
    coord_list = coords.split(",")
-   xy_list = []
-   for i in range(0, len(coord_list), 2):
-       x, y = coord_list[i:i+2]
-       xy_list.append([int(x), int(y)])
+   xy_list = [int(coord_list[i]), int(coord_list[i+1]) 
+              for i in range(0, len(coord_list), 2)]
    return xy_list

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

test_judge(testcases)

xy 文字座標を ":" で区切って連結するデータ構造

上記の方法の 問題点 は、x 座標と y 座標を 区切る文字列 と、xy 文字座標を 区切る文字列 に、同じ文字列を使う ことが 原因 でした。従って、この問題は、その 2 つの文字列を 別の文字列にする ことで 改善 することができます。

下記は、xy 文字座標":" で区切って連結 するデータ構造でテストケースを記述するプログラムです。

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

上記では、xy 文字座標を区切る文字列を ":" に変更していますが、x 座標と y 座標区切る文字列 を変えても、":" 以外別の文字 を使うように 変えてもかまわない ので、興味がある方は試してみると良いでしょう。例えば、x 座標と y 座標を区切る文字列を "_" に変更 した場合、〇の勝利のテストケースのデータは、"0_0,0_1,1_0,1_1,2_0" のようになります。どの文字列を使えば わかりやすくなる かは、人によって 感じ方が異なる と思いますので、このデータ構造を採用したいと思った場合は、様々な 区切りの 文字を試して 下さい。

個人的 にはどの区切り文字の組み合わせを使っても、あまり わかりやすくはならない ような 気がします ので、本記事ではこのデータ構造は採用しません。

このデータ構造の場合は、test_judge修正 は、下記のプログラムのように、split(",")split(":")修正するだけ で済みます。

def test_judge(testcases):
    for testcase in testcases:
        testdata, winner = testcase
        mb = Marubatsu()
        for coord in testdata.split(":"):
            x, y = xytext_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.split(","):
+       for coord in testdata.split(":"):
        x, y = xytext_to_xy(coord)            
            mb.move(x, y)
        print(mb)

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

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

test_judge(testcases)

xy 文字座標のデータ構造その 3(数学の座標)

今回の記事で紹介した xy 座標のデータ構造は、いずれも x 座標と y 座標の 区別がつきづらかったり、xy 文字座標を連結する際に、区切りの文字列を工夫 する必要があるなどの 欠点 がありました。その問題を解決する方法として、数学の座標 と同様に、"(1,0)" のように、前後"("")"囲った文字列 で記述するという データ構造 が考えられます。

利点と欠点

このデータ構造の利点と欠点は以下の通りです。

利点

  • x 座標と y 座標区切りがわかりやすい
  • x 座標と y 座標に、任意の桁数の数字 を記述できる
  • xy 文字座標連結 した際の 区切りがわかりやすい
  • 区切りの記号を "," で統一 しても わかりづらくならない

欠点

  • 記述が ",""("")" を記述する分だけ かなり長くなる。具体的には、複数xy 文字座標文字列で記述 する場合と、複数xy 座標list で記述 する場合の 記述量ほとんど変わらない
  • データの変換アルゴリズムが複雑 になる

上記の利点と欠点については、この後で具体例を挙げて詳しく説明します。

xytext_to_xy の定義

上記のデータ構造は、先程の x 座標と y 座標を "," で区切って連結 したデータ構造に対して、両端"("")"追加 したものです。そのため、xytext_to_xy最初 に、両端の文字を削除 する 処理を追加 するという 修正 を行うだけで済みます。

文字列の 最初の文字最後の文字削除 する処理は、スライス表記を使って、下記のプログラムのように記述することができます。以前の記事 で説明したように、[1:-1] は、シーケンス型1 番 の要素、すなわち 先頭(0 番)の次 の要素 から最後(-1 番)の直前 の要素 まで を要素として持つ、新しい 同じ種類の データを作成する という意味を持つので、これを使うことで、両端の文字を除いた 文字列を得ることが出来ます。

print("ABCDE"[1:-1])

実行結果

BCD

下記は、スライス表記を使って、xytext_to_xy を修正したプログラムです。

def xytext_to_xy(coord):
    x, y = coord[1:-1].split(",")
    return int(x), int(y)
修正箇所
def xytext_to_xy(coord):
-   x, y = coord.split(",")
+   x, y = coord[1:-1].split(",")
    return int(x), int(y)

下記のプログラムは、"(1,2)" を、上記の xytext_to_xy を使って xy 座標に変換するプログラムです。実行結果から正しく動作することが確認できます。

print(xytext_to_xy("(1,2)"))

実行結果

(1, 2)

xy 文字座標を直接連結するデータ構造

先程と同様に、複数の xy 文字座標をまとめて扱うデータ構造として、xy 文字座標を直接連結する方法と、"," 等の文字列で区切って連結する方法を考えることにします。

下記は、xy 文字座標を 直接連結 するデータ構造でテストケースを記述するプログラムです。xy 座標 どうしの 区別わかりやすく なりますが、記述がかなり長く なります。

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

下記は、〇の勝利のテストケースを、上記のデータ構造 で記述した場合、間に "," を記述して連結 した場合、xy 座標を要素とする list で記述した場合を並べたものです。

3 行目のように、間に空白を記述せずlist で記述 した場合の 記述の長さ1 行目長さあまり変わりません。また、2 行目の記述同じ長さ になります。このように、数学の座標と同じ文字列 で座標を記述する方法は、記述の短さ という 利点得られません

"(0,0)(0,1)(1,0)(1,1)(2,0)"
"(0,0),(0,1),(1,0),(1,1),(2,0)"
[[0,0],[0,1],[1,0],[1,1],[2,0]]

〇×ゲームの場合は、x 座標と y 座標を表す文字列が 必ず 1 文字 になるので、xy 文字座標 の文字列の 長さ"("","")"3 文字 を足すと、必ず 5 文字 になります。従って、text_to_list は、下記のプログラムのように、25修正 する だけで済みます

def text_to_list(coords):
    coord_list = [ coords[i:i+5] for i in range(0, len(coords), 5) ]
    return coord_list
修正箇所
def text_to_list(coords):
   coord_list = [ coords[i:i+2] for i in range(0, len(coords), 2) ]
+   coord_list = [ coords[i:i+5] for i in range(0, len(coords), 5) ]
    return coord_list

test_judge を下記のように、xy 文字座標を直接連結 する場合の 処理に戻して から、test_judge を実行することで、修正したデータ構造で、test_judge が正しく動作することが確認できます。実行結果は、先ほどと同じなので省略します。

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

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

test_judge(testcases)

2 桁以上の場合の xy 文字座標を直接連結するデータ構造

上記の、text_to_list は、囲碁のように、x 座標や y 座標の桁数が 2 桁以上 になる場合は、xy 座標の文字列が 6 文字以上になる ため 使うことが出来ません。〇×ゲームには必要はありませんが、参考までに、そのような場合の text_to_list の定義を紹介します。

"(0,0)(0,1)(1,0)(1,1)(2,0)" をよく見ると、それぞれの xy 文字座標が、")(" という 2 文字 の文字列で 区切られている ことがわかります。従って、split メソッドの 実引数")(" を記述 することで、この文字列を、xy 文字座標 を要素として持つ list に変換 することができます。下記のプログラムは、実際に split メソッドを使って "(0,0)(0,1)(1,0)(1,1)(2,0)" を list に変換するプログラムです。

print("(0,0)(0,1)(1,0)(1,1)(2,0)".split(")("))

実行結果

['(0,0', '0,1', '1,0', '1,1', '2,0)']

一見する とうまくいっているように 見えるかも しれませんが、よく見る と、最初最後 の要素に、"(0,0"2,0)" のように余計な () がデータに 入っている ことがわかります。ピンとこない方は、下記の、各要素を表示 するプログラムを実行してみて下さい。

xytext_list = "(0,0)(0,1)(1,0)(1,1)(2,0)".split(")(")
for xytext in xytext_list:
    print(xytext)

実行結果

(0,0
0,1
1,0
1,1
2,0)

この問題は、元の文字列先頭(最後)削除 することで解決でき、その方法は、先程の xytext_to_xy で紹介済で、下記のプログラムのように [1:-1] を記述します。

xytext_list = "(0,0)(0,1)(1,0)(1,1)(2,0)"[1:-1].split(")(")
for xytext in xytext_list:
    print(xytext)
修正箇所
- xytext_list = "(0,0)(0,1)(1,0)(1,1)(2,0)".split(")(")
+ xytext_list = "(0,0)(0,1)(1,0)(1,1)(2,0)"[1:-1].split(")(")
for xytext in xytext_list:
    print(xytext)

実行結果

0,0
0,1
1,0
1,1
2,0

従って、text_to_list は、下記のプログラムのように定義することが出来ます。

def text_to_list(coords):
    coord_list = coords[1:-1].split(")(")
    return coord_list
修正箇所
def text_to_list(coords):
-   coord_list = [ coords[i:i+5] for i in range(0, len(coords), 5) ]
+   coord_list = coords[1:-1].split(")(")
    return coord_list

ただし、このまま test_judge実行 すると、下記のプログラムのように エラーが発生 してしまいます。その理由について少し考えてみて下さい。

test_judge(testcases)

実行結果

略
c:\Users\ys\ai\marubatsu\029\marubatsu.ipynb セル 35 line 3
      1 def xytext_to_xy(coord):
      2     x, y = coord[1:-1].split(",")
----> 3     return int(x), int(y)

ValueError: invalid literal for int() with base 10: ''

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

  • ValueError
    値(value)に関するエラー
  • invalid literal for int() with base 10: ''
    int() の実引数に、基数(base)を 10 とした場合の無効な(invalid)リテラルである '' が記述されている

注:with base 10 の「基数を 10 とする」は、10 進数 のことを表します。説明していませんでしたが、組み込み関数 int は、10 進数以外 の文字列を 数値型 のデータに 変換 することができます。詳細は下記のリンク先を参照して下さい。

エラーの原因は、修正後text_to_list では、xy 文字座標の先頭と最後にあった "("")" を削除 しているからです。xytext_to_xy は、xy 文字座標の先頭と最後に "("")" が _記述 されているという 前提 で処理を行っているので、下記のプログラムのように xytext_to_xy元に戻す 必要があります。

def xytext_to_xy(coord):
    x, y = coord.split(",")
    return int(x), int(y)
修正箇所
def xytext_to_xy(coord):
-   x, y = coord[1:-1].split(",")
+   x, y = coord.split(",")
    return int(x), int(y)

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

test_judge(testcases)

xy 文字座標を "," で区切って連結するデータ構造

下記は、xy 文字座標を "," で区切って連結 するデータ構造でテストケースを記述するプログラムです。このデータ構造の場合は、引き分けのテストケースのように、座標の数が増える と文字列の 長さかなり長く なってしまいます。

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

データ構造が変わったので、test_judge修正 する必要がありますが、x 座標と y 座標を区切る場合と、xy 文字座標を区切る場合で、同じ ","使われている ので 工夫が必要 になります。まず、split を使う方法について紹介します。

split を使った text_to_list の定義

"(0,0),(0,1),(1,0),(1,1),(2,0)" をよく見ると、xy 文字座標区切る ","前後 に、必ず ")""("記述 されていることがわかります。そのため、先程と同様 に、"),(" という 3 文字 の文字列を 実引数 に記述して split メソッド を呼び出すことで、xy 文字座標要素 とする list を作成する ことができます。全体の文字列の 先頭"("末尾")"削除する必要がある 点も 先ほどと同様 です。下記は、そのように text_to_list を修正したプログラムです。なお、他の関数を修正する必要はありません。

def text_to_list(coords):
    coord_list = coords[1:-1].split("),(")
    return coord_list
修正箇所
def text_to_list(coords):
-   coord_list = coords[1:-1].split(")(")
+   coord_list = coords[1:-1].split("),(")
    return coord_list

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

test_judge(testcases)

split を使わない text_to_list の定義

split メソッドを使った修正の方が簡単ですが、参考までにsplit を使わない場合の text_to_list の定義の方法について紹介します。

xy 文字座標を 直接連結 した場合は、xy 文字座標5 文字の文字列 であることに 注目 して、下記のような text_to_list を定義して利用しました。下記のプログラムは、文字列の先頭から、5 文字おき に、5 文字分 を取り出すという処理を行っています。

def text_to_list(coords):
    coord_list = [ coords[i:i+5] for i in range(0, len(coords), 5) ]
    return coord_list

xy 文字座標を直接連結した場合は、"," という 1 文字が入る ので、6 文字おき に、5 文字分 を取り出すように修正すれば良いことがわかります。下記は、text_to_list をそのように修正したプログラムです。

def text_to_list(coords):
    coord_list = [ coords[i:i+5] for i in range(0, len(coords), 6) ]
    return coord_list
修正箇所
def text_to_list(coords):
-   coord_list = [ coords[i:i+5] for i in range(0, len(coords), 5) ]
+   coord_list = [ coords[i:i+5] for i in range(0, len(coords), 6) ]
    return coord_list

このまま test_judge を実行すると エラーが発生 します。その理由は、先程 xytext_to_xy を、xy 文字座標の 先頭最後"("")"記述されていない場合 の処理を行うように 修正 してしまったためです。下記のように xytext_to_xy修正し直して ください。

def xytext_to_xy(coord):
    x, y = coord[1:-1].split(",")
    return int(x), int(y)
修正箇所
def xytext_to_xy(coord):
-   x, y = coord.split(",")
+   x, y = coord[1:-1].split(",")
    return int(x), int(y)

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

test_judge(testcases)

1 行に記述 するプログラムが 長くなる とプログラムが 見づらく なります。また、1 画面に収まらない ような長さになった場合は、スクロールが必要 になります。

そのような場合は、途中で改行 を行うことで、プログラムを 読みやすくする ことが出来ます。ただし、"" で囲ったリテラルで 文字列を記述 する場合は、途中で改行 を行うことが 出来ない ので、工夫が必要 になります。具体的には、文字列を いくつかに分けて、それぞれを + 演算子 を使って 連結 することで、+ 演算子前後で改行 を行うことができるようになります。下記は、引き分けのテストケースの部分の 文字列2 つに分けて改行 した場合のプログラムです。

    [                                                            # 着手順とゲーム盤
        "(0,0),(0,1),(1,0),(1,1),(2,1)," + 
        "(2,0),(0,2),(1,2),(2,2)",                               # 136  oox 
        Marubatsu.DRAW,                                          # 245  xxo
    ],    
修正箇所
    [                                                            # 着手順とゲーム盤
-       "(0,0),(0,1),(1,0),(1,1),(2,1),(2,0),(0,2),(1,2),(2,2)", # 136  oox 
+       "(0,0),(0,1),(1,0),(1,1),(2,1)," + \
+       "(2,0),(0,2),(1,2),(2,2)",                               # 136  oox 
        Marubatsu.DRAW,                                          # 245  xxo
    ],  

なお、Python では、文の途中で改行 する際に、改行の直前\ を記述する必要 がありますが、list のリテラル の中で 要素を記述 する際は、\ を記述せずに 途中で 改行 することが できます。下記のプログラムは、代入文の途中で改行 しているので実行すると エラーが発生 しますが、上記のプログラムは、list の要素を記述 する際に 改行 を行っているので、エラーにはなりません

なお、list の要素を記述する際に、改行の前に \ を記述しても エラーにはならない ので、覚えられない 場合は、常に 改行の前に \ を記述 しても 構いません

a = "ABC" +
"DEF"

実行結果

  Cell In[45], line 1
    a = "ABC" +
               ^
SyntaxError: invalid syntax

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

  • SyntaxError
    構文(文法のこと)(syntax)に関するエラー
  • invalid syntax
    (文の途中で改行されるという)不正な(invalid)文法(syntax)

今回の記事のまとめ

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

下記は、今回の記事で紹介した データ構造表にまとめた ものです。参考までに、一番下の行に、複数の xy 座標を list で表現 したデータ構造を入れてあります。

短さ と、分かりやすさ記号 は、◎、〇、△、×、×× の順で評価が高いという意味を表します。分かりやすさの記号は 筆者の主観 なので、人によって感じ方が違うでしょう。

桁数の列で、1 桁のみ と書かれている場合でも、工夫次第 では複数の桁数や、任意の桁数を記述することが出来る場合があります。

xy座標 複数のxy 座標 記述例 短さ 分かりやすさ 桁数
直接連結 直接連結 "0011" 1 桁のみ
直接連結 "," で区切る "00,11" 1 桁のみ
"," で区切る 直接連結 "0,01,1" ×× 1 桁のみ
"," で区切る "," で区切る "0,0,1,1" × 任意
"," で区切る ":" で区切る "0,0:1,1" 任意
数学と同じ 直接連結 "(0,0)(1,1)" × 任意
数学と同じ "," で区切る "(0,0),(1,1)" ×× 任意
list list [[0,0],[1,1]] ×× 任意

個人的 には、テストケース のデータを プログラム内に直接記述 するためのデータ構造としては、上記の表の中では 2 番目の "00,11"最も優れている のではないかと 思います が、前回の記事で紹介した Excel 座標"," で区切る "A1,B2" のほうさらに優れている のではないかと 思います ので、本記事ではそちらの方を採用することにします。

採用しないデータ構造 を 1 回分の記事を使って 紹介 したのは、無駄ではないか と思っている人がいるかもしれません。今回の記事で紹介したデータ構造は、確かに テストケースを記述する際は 大きな利点はない かもしれませんが、別の目的 で利用する際に、今回紹介 したデータ構造 のほうがExcel 座標よりも適している 場合が実際にあります。

具体例 として、例えば 囲碁のゲーム盤 のサイズは 19 x 19 ですが、Excel 座標(15, 16) をどのように 表記するかすぐにわかる人どれだけいる でしょうか?〇×ゲーム であれば、"A""B""C"3 つしか アルファベットが 使われない ので すぐに慣れる ことができますが、アルファベット15 番目の文字 が何であるかを すぐに思い浮かべる ことは 困難 なので、Excel 座標囲碁の座標を記述 するのは 容易ではない でしょう。

個人的には、チェスや将棋以上の大きさのゲーム盤の座標を記述する場合は、Excel 座標よりも、xy 文字座標の方が扱いやすくなるような気がしますが、チェスの棋譜に親しんでいる人にとっては Excel 座標のような2座標の方が扱いやすいのではないかと思います。

これまでの記事で何度も説明しているように、様々なデータ構造 と、そのデータ構造を処理するための 様々なアルゴリズム を知っておくことは、状況に応じて 自分にとって最も適した アルゴリズムとデータ構造を選択 できるようになるために 重要 なことなのです。

これまでの記事では、座標を表すデータを、x 座標と y 座標を使って表現するというデータ構造で表現してきましたが、次回の記事では それらとは異なるデータ構造 について紹介します。次回で座標を表すデータ構造の紹介を終え、テストの続きを行う予定です。

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

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

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

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

次回の記事

  1. int は、半角 と、全角両方 の空白文字を 無視 します

  2. チェスでは、一番下の行の y 座標を 1 と表記するので、Excel 座標と比べて 上下が逆 になります

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