目次と前回の記事
実装の進捗状況と前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤のすべてのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
先手は 〇 のプレイヤーである
- プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
- すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
なお、前回の記事で採用しなかった関数は、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 に変換する処理を、以下の いずれかの方法 で行うことが出来ます。
-
excels_to_list
を、名前を変えずに そのまま利用 する -
xytext_to_list
という名前で、excels_to_list
と 同じ処理 を行う 関数を定義 する -
文字列を list に変換 する処理を行うので、
excels_to_list
をtext_to_list
という 名前に変更 して利用する。この場合は、前回のプログラムも修正 する必要がある -
名前だけ が気になる場合は、下記のプログラムのように、
xytext_to_list
にexcels_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
という 名前が変 になってしまうので、仮引数の名前 と ローカル変数の名前 の excels
を coords
に、下記のプログラムのように 修正 します。
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_list
と excel_to_xy
の 2 箇所 です。
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
の場合と異なり、x
と y
には、任意の文字数 のデータが代入されます。また、int
は 任意の文字数 の文字列を 整数に変換 できるので、このデータ構造は、下記のプログラムの実行結果からわかるように、任意の桁数 の x 座標と y 座標を 記述 することができます。
print(xytext_to_xy("10,200"))
実行結果
(10, 200)
split
メソッド で 文字列 を list に変換 した場合は、数値の前後 に 空白文字1を記述 することが できる という利点も得られます。その理由は、組み込み関数 int
が、先頭 と 末尾 の 空白文字を無視 するからです。下記は、先頭と末尾に半角の 空白文字が 5 つ 記述された 文字列 を、整数型 のデータに 変換 するプログラムです。
print(int(" 100 "))
実行結果
100
従って、下記のプログラムのように、中に 空白文字が記述 された、"10 , 20"
を xytext_to_xy
で xy 座標に変換 することが出来ます。
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 つの座標 を表していますが、このデータ構造の事を知らない人は、0
、00
、11
、01
、12
、0
の 6 つのデータが記述 されている ようにしか見えない と思います。
また、このことは、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
のブロック内の 2
を 3
に修正します。
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
を実装することは できません。
一つの方法として、split
で list に変換 したデータを、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 座標を表す文字列を
x
とy
に代入する -
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_xylist
でtest_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
は、下記のプログラムのように、2
を 5
に 修正 する だけで済みます。
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 などは省略します。
次回の記事