目次と前回の記事
実装の進捗状況と前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤のすべてのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
先手は 〇 のプレイヤーである
- プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
- すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
なお、前回の記事で採用しなかった関数は、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 == 0
、coord == 1
、coord == 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
チェックする 数値座標 の 順番 を 0
、1
、2
のように 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_xy
を 1 行で記述 することもできますが、プログラムが わかりにくくなる という欠点があります。
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
を表示 すると、実行結果のように、0
、1
、2
が表示されます。そのため、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"
が記述された場合に、1
と 2
の 2 つ の 数値座標 が記述されているのか、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
演算子 で、下記のように記述することで 調べる ことができます。計算結果は 同様 に True
と False
です。
データ not in シーケンス型
in
と not in
演算子は、dict などの マッピング型 に対しても 利用できます。
シーケンス型に対する in
演算子の詳細については、下記のリンク先を参照して下さい。
break
文と continue
文
for 文などによる 繰り返し処理 の ブロックの中 で、break
を記述 することで、繰り返し処理 を 終了 することができます。break
で 1 つの処理 を表すので、break 文 と呼びます。
下記のプログラムは、for 文で 0
から 9
まで の 繰り返し処理 を行いますが、for 文の ブロックの中 の if 文 で、i
が 5
になった場合 に 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 などは省略します。
次回の記事