目次と前回の記事
前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤のすべてのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
先手は 〇 のプレイヤーである
プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
なお、今回の記事では test.py は使用しないので test.py は割愛します。
前回までのおさらい
前回の記事までで、judge
メソッドを実装するための、さまざまなアルゴリズムを紹介しました。今回の記事では〇×ゲームを実際に遊ぶためのメソッドを定義します。
〇×ゲームを遊ぶためのメソッドの定義
前回の記事で、〇×ゲームの すべての仕様 の 実装が完了 しました。現時点では、〇×ゲームの AI を実装していないので、AI を相手に〇×ゲームの対戦を行うことはまだできませんが、〇 と × の 両方 を 人間が担当 すれば 〇×ゲームを 実際に遊ぶ ことができます。
しかし、現状では、〇×ゲームを遊ぶために、Marubatsu
クラスの インスタンスを作成した後 で、下記の アルゴリズム で Python の プログラムを記述 し、実行する必要 があります。
-
restart
メソッドを使って、〇×ゲームを 再起動 する -
print
を使って ゲーム盤を表示 する - プレイヤーに 着手を行う 座標を入力 させる
- プレイヤーが 入力した座標 に
move
メソッドで 着手を行う -
judge
メソッドで 勝敗判定 を行い、ゲームの 決着がついていない場合 は 手順 2 へ戻る
下記のプログラムは、上記の アルゴリズム を 実際に記述 して〇×ゲームを遊んだ場合の 例 ですが、〇×ゲームを 遊ぶ際 に、下記のようなプログラムを 記述して実行 するのは 大変 なので、このままでは〇×ゲームを 気軽に遊ぶ ことは できません。そこで、〇×ゲームを遊ぶ際に必要となる、上記の アルゴリズム の 処理 を行う メソッドを定義 することにします。
なお、下記のプログラムのそれぞれの処理が、上記のアルゴリズムのどの部分に対応しているかについて、コメントに記しました。
from marubatsu import Marubatsu
# Marubatsu クラスのインスタンスを作成する
mb = Marubatsu()
# ここから〇×ゲームのプレイを開始する
# 手順 1:〇× ゲームを再起動する
mb.restart()
# 手順 2:ゲーム盤の表示
print(mb)
# 手順 3:(1, 1) への着手を表すデータの入力
x = 1
y = 1
# 手順 4:move メソッドによる着手
mb.move(x, y)
# 手順 5:judge メソッドによる勝敗判定
print(mb.judge())
# 上記で決着がついていないことを表す playing が表示されるので手順 2 に戻る
# 手順 2:ゲーム盤の表示
print(mb)
# 手順 3:(0, 0) への着手を表すデータの入力
x = 0
y = 0
# 手順 4:move メソッドによる着手
mb.move(x, y)
# 手順 5:judge メソッドによる勝敗判定
print(mb.judge())
# 以下決着がつくまで繰り返す
実行結果
Turn o
...
...
...
playing
Turn x
...
.o.
...
playing
play
メソッドの定義
具体的には、以下のような メソッドを定義 します。
- 処理:〇×ゲームを遊ぶために必要な上記の 手順 1 ~ 5 の処理 を行う
-
名前:〇×ゲームを 遊ぶ (play)ための 処理 を行うので、
play
とする - 入力:なし
- 出力:なし
なお、play
メソッドは 返り値を返さない ので 出力 の所に「なし」と記述しましたが、画面に文字列を表示 するという 意味 での 出力は行います。
このメソッドの定義は下記のプログラムのようになります。今回の記事では、手順 1 から順番 に play
メソッドを 実装 を行います。
def play(self):
# 手順 1 の処理
# 手順 2 の処理
# 手順 3 の処理
# 手順 4 の処理
# 手順 5 の処理
Marubatsu.play = play
手順 1 と 2 の実装
手順 1 の「restart
メソッドを使って、〇×ゲームを再起動する」は、下記のプログラムの 3 行目のように記述できます。play
メソッドを 呼び出した際 に、仮引数 self
に、play
メソッドを 呼び出した Marubatsu
クラスの インスタンスが代入 されるので、self.restart()
によって 〇×ゲームの再起動 を行うことができます。
手順 2 の「print
を使ってゲーム盤を表示する」は、下記のプログラムの 5 行目のように記述できます。特に難しい点はないと思いますので説明は省略します。
1 def play(self):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲーム盤の表示
5 print(self)
6 # 手順 3 の処理
7 # 手順 4 の処理
8 # 手順 5 の処理
9
10 Marubatsu.play = play
行番号のないプログラム
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲーム盤の表示
print(self)
# 手順 3 の処理
# 手順 4 の処理
# 手順 5 の処理
Marubatsu.play = play
下記は、play
メソッドの手順 1 と 2 の処理が 正しく実装 できたかどうかを 確認 するためのプログラムです。3 行目で、(1, 1) のマスに 着手 を行っているのは、play
メソッドを実行することで、手順 1 の 〇×ゲームの 再起動 が行われているかどうかが 確認 できるようにするためです。実行結果から、〇 の手番 で、マークが 一つも配置されていない ゲーム盤が 表示 されるので、手順 1 と 手順 2 が正しく実装されていることが確認できます。
mb = Marubatsu()
mb.move(1, 1)
mb.play()
実行結果
Turn o
...
...
...
手順 3 の実装と組み込み関数 input
手順 3 の「プレイヤーに着手を行う座標を入力させる」を実装するためには、プレイヤーが どのような方法 で 座標を入力 するかについての、ユーザーインタフェース(以下 UI と表記します)を 決める 必要があります1。現時点では、〇×ゲームの ゲーム盤を表示 する UI は、文字だけ で行う CUI なので、座標の入力 の UI、文字だけを使った CUI で実装 することにします。なお、ゲーム盤を画像で表示し、マウスを使って座標を入力する GUI については今後の記事で実装する予定です。
Python には、キーボードから入力 した 文字列 を 返り値として返す という、CUI の入力処理 を行う input
という 組み込み関数 があります。VSCode の JupyterLab2 では、input
を呼び出す すると、下図のように VSCode の タイトルバーの付近 に テキストボックスが表示 され、そのテキストボックスにキーボードから 文字を入力 できるようになります。
print(input())
input
を呼び出すと、上図に記述されているように、Enter キー または、Escape キー を 押すまで プログラムの 処理が中断 され、Enter キー を押した場合はテキストボックスに 入力した文字列 が、Escape キー を押した場合は 空文字 が 返り値 として返されます。
下記は、上記のプログラムを実行し、テキストボックスに "abc"
を入力 して Enter キー を押した場合の実行結果です。
abc
input
の詳細については、下記のリンク先を参照して下さい。
手順 3 は、下記のプログラムの 7 行目のように実装できます。下記のプログラムでは、キーボードから入力した座標を表す文字列を coord
という ローカル変数に代入 しています。
1 def play(self):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲーム盤の表示
5 print(self)
6 # キーボードからの座標の入力
7 coord = input()
8 # 手順 4 の処理
9 # 手順 5 の処理
10
11 Marubatsu.play = play
行番号のないプログラム
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input()
# 手順 4 の処理
# 手順 5 の処理
Marubatsu.play = play
修正箇所
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
+ coord = input()
# 手順 4 の処理
# 手順 5 の処理
Marubatsu.play = play
input
に関する注意点
input
の 返り値 は 常に文字列型 のデータになる点に 注意が必要 です。
例えば、下記のプログラムを実行し、input
で表示されるテキストボックスに 1
を入力して Enter キーを押すと下記のような エラーが発生 します。
print(input() + 2)
実行結果
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\037\marubatsu.ipynb セル 7 line 1
----> 1 print(input() + 2)
TypeError: can only concatenate str (not "int") to str
上記のエラーメッセージは、以下のような意味を持ちます。
-
TypeError
データ型(type)に関するエラー -
can only concatenate str (not "int") to str
文字列型(str)には、(整数型ではなく(not "int"))文字列型(str)のみ(only)結合(concatenate)できる(can)
input
で表示されるテキストボックスに 1
のような、数値に見える ような データを入力 しても、input
の 返り値 は 必ず "1"
のような 文字列型のデータ になります。 従って、input
で 入力した文字列 を、数値として扱いたい 場合は、int
や float
を使って、下記のプログラムのように 整数型 や 浮動小数点数型 のデータに 変換する必要 があります。
print(int(input()) + 2)
実行結果(テキストボックスに 1
を入力して Enter キーを押した場合)
3
同様の理由 で、テキストボックスに [1, 2]
のようなデータを記述しても、input()
の返り値が list になる ことは ありません。この場合は "[1, 2]"
という 文字列 が返ります。
input
の返り値 は、どのような場合でも 必ず文字列型 のデータになる。
input
で入力した文字列を、数値型 や list などの、他のデータ型 として 扱いたい場合 は、明示的な型変換 を行う必要がある。
手順 4 の実装
手順 4 の「プレイヤーが入力した座標に move
メソッドで着手を行う」では、手順 3 でテキストボックスに 入力した座標に、move
メソッドで 着手を行う 必要がありますが、そのためにはテキストボックスに入力する 座標のデータ構造(どのように記述するか)を 決める必要 があります。どのようなデータ構造にすればよいかについて少し考えてみて下さい。
座標 を表す 文字列 の _データ構造 として、以下のようなものが考えられるでしょう。
-
数学の座標 と同様に、
(1, 2)
のように記述する - 上記と同様だが、外側の
(
と)
を省略 して1,2
のように記述する - テストケースを記述する際に利用した
A1
のような Excel 座標として記述する
上記のいずれの方法を使ってもかまいませんが、本記事では 2 番目 の 1,2
のように記述するデータ構造を 採用 することにします。3 番目の Excel 座標を採用したい人は、以前の記事 で定義した excel_to_xy
を利用する必要があります。また、上記以外で、採用したいデータ構造を思いついた方は、そのデータ構造を採用したプログラムを記述してみて下さい。
2 番目のデータ構造を採用した 理由 は、以前の記事 で紹介した split
メソッドを使って、下記のプログラムの 9 行目ように、文字列 を x 座標 と y 座標 を 要素 とする list に変換 し、list の展開 を使って x
と y
に それぞれの座標を代入 できるからです。
x
、y
に代入されたデータは 文字列型 なので、11 行目で move
を使って (x, y) に着手を行う際に、int
を使って 整数型 のデータに 変換 する 必要がある点 に注意して下さい。
1 def play(self):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲーム盤の表示
5 print(self)
6 # キーボードからの座標の入力
7 coord = input()
8 # x 座標と y 座標の計算
9 x, y = coord.split(",")
10 # (x, y) に着手を行う
11 self.move(int(x), int(y))
12 # 手順 5 の処理
13
14 Marubatsu.play = play
行番号のないプログラム
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input()
# x 座標と y 座標の計算
x, y = coord.split(",")
# (x, y) に着手を行う
self.move(int(x), int(y))
# 手順 5 の処理
Marubatsu.play = play
修正箇所
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input()
# x 座標と y 座標の計算
+ x, y = coord.split(",")
# (x, y) に着手を行う
+ self.move(int(x), int(y))
# 手順 5 の処理
Marubatsu.play = play
下記は、play
メソッドを呼び出し、2 行目でゲーム盤を表示することで、手順 4 の着手 が正しく行われたかどうかを 確認 するプログラムです。下記は、テキストボックスに 1,1
を入力した場合の実行結果で、手順 3、4 の処理が正しく行われたことが確認できます。
mb.play()
print(mb)
実行結果
Turn o
...
...
...
Turn x
...
.o.
...
input
でメッセージを表示する方法
テキストボックスに入力する 座標のデータ構造 は、play
メソッドを記述した人は当然知っていますが、play
メソッドを 利用するだけの人 はそのことを 知りません 。そのため、座標の 入力方法 という、ゲームの遊び方 を知らせたほうが 親切 です。
input
の 実引数 に 文字列を記述 することで、記述した文字列 が、下図のように テキストボックスの下に表示 されるので、これを利用すると良いでしょう。
input("x,y の形式で座標を入力して下さい")
下記はそのように play
メソッドを修正したプログラムです。
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい")
# x 座標と y 座標の計算
x, y = coord.split(",")
# (x, y) に着手を行う
self.move(int(x), int(y))
# 手順 5 の処理
Marubatsu.play = play
修正箇所
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
- coord = input()
+ coord = input("x,y の形式で座標を入力して下さい")
# x 座標と y 座標の計算
x, y = coord.split(",")
# (x, y) に着手を行う
self.move(int(x), int(y))
# 手順 5 の処理
Marubatsu.play = play
下記のプログラムを実行することで、修正後に play
メソッドが正しく動作することを確認できます。実行結果は先ほどと同様なので省略します。
mb.play()
print(mb)
手順 5 の実装と while
文
手順 5 の「judge
メソッドで勝敗判定を行い、ゲームの決着がついていなければ手順 2 へ戻る」という処理は、別の言葉で 言い換える と、「〇×ゲームの 決着がつくまで、手順 2 ~ 5 の処理を 繰り返す」という処理です。
これまでに利用してきた、for 文 では、for i in range(10)
などのように、あらかじめ 繰り返す 回数 が 決まっている ような 繰り返し処理 を 記述 してきました。
一方、〇×ゲームは、決着がつくまで 何回 着手を 行う必要があるか は、ゲームを行ってみなければ わからない ので、手順 2 ~ 5 の処理を 何回繰り返せばよいか を、繰り返し処理を行う前に あらかじめ知る ことは 不可能 です。
このような、繰り返す回数 を あらかじめ知ることができない ような繰り返しは、while 文 で記述します。while 文は下記のように記述し、条件式 が True
の間、while 文の ブロック に記述された処理を 繰り返し実行 します。
while 条件式:
条件式が True の間繰り返し処理を行うブロック
下図は while 文のフローチャートです。
先程のアルゴリズムの 手順 2 ~ 5 は、ゲームの決着がつくまで 無限に 手順 2 ~ 4 の処理を 繰り返す というアルゴリズムです。無限に 処理を 繰り返す ような 繰り返し処理 の事を、無限ループ と呼びます。while 文 は、条件式 が True
の間 繰り返し処理を行うので、下記のようなプログラムで、無限ループを記述 できます。
while True:
while 文のブロックの処理
単に無限ループを記述しただけでは、プログラムの 処理が永遠に終わらない ことになってしまいます。そのため 無限ループ が 記述 された プログラムを実行 すると、プログラムが 固まって動かななくなる という バグが発生 します。そのため、while 文 の 条件式 に True
を記述 した場合は、while 文の ブロックの中 で、特定の条件が満たされた場合 に break 文 を 記述する ことで、無限ループ から 抜け出す ような処理を 記述する必要 があります。
while 文のブロックを何度繰り返しても while 文 の 条件式 が False
にならない ような場合も 無限ループになります。while 文 は、for 文と比べて 無限ループが発生しやすい ので、while 文を記述する際には 無限ループが発生しない ように 注意しながら プログラムを 記述する必要 があります。
上記のことから、手順 2 ~ 5 の処理は、下記のプログラムのように記述できます。
- 5 行目:while 文による 無限ループを記述 する
- 7 ~ 13 行目:元のプログラムに インデントを入れて、while 文 の ブロックの処理にする
-
15、16 行目:
judge
メソッドの 返り値 がMarubatsu.PLAYING
以外 の場合は、ゲームの 決着がついた場合 なので、16 行目で break 文 を記述して 無限ループを終了 する
1 def play(self):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # 無限ループを行う
5 while True:
6 # ゲーム盤の表示
7 print(self)
8 # キーボードからの座標の入力
9 coord = input("x,y の形式で座標を入力して下さい")
10 # x 座標と y 座標の計算
11 x, y = coord.split(",")
12 # (x, y) に着手を行う
13 self.move(int(x), int(y))
14 # ゲームの決着がついた場合は、無限ループを抜ける
15 if self.judge() != Marubatsu.PLAYING:
16 break
17
18 Marubatsu.play = play
行番号のないプログラム
def play(self):
# 〇×ゲームを再起動する
self.restart()
# 無限ループを行う
while True:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい")
# x 座標と y 座標の計算
x, y = coord.split(",")
# (x, y) に着手を行う
self.move(int(x), int(y))
# ゲームの決着がついた場合は、無限ループを抜ける
if self.judge() != Marubatsu.PLAYING:
break
Marubatsu.play = play
修正箇所(while 文のブロックのインデントの追加に関する修正箇所は、表示するとわかりづらくなるので省略しました)
def play(self):
# 〇×ゲームを再起動する
self.restart()
# 無限ループを行う
+ while True:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい")
# x 座標と y 座標の計算
x, y = coord.split(",")
# (x, y) に着手を行う
self.move(int(x), int(y))
# ゲームの決着がついた場合は、無限ループを抜ける
+ if self.judge() != Marubatsu.PLAYING:
+ break
Marubatsu.play = play
下記のプログラムを実行し、0,0
、0,1
、1,0
、1,1
、2,0
の順でテキストボックスに入力すると、下記のような実行結果が表示されます。(2, 0) に 着手を行う 事で 〇が勝利する ため 16 行目の break 文が実行 されるため、無限ループが終了 します。そのことは、テキストボックスが表示されなくなる ことで確認できます。
mb.play()
実行結果
Turn o
...
...
...
Turn x
o..
...
...
Turn o
o..
x..
...
Turn x
oo.
x..
...
Turn o
oo.
xx.
...
手順 5 の別の記述方法
上記のプログラムの アルゴリズム は、無限ループ の ブロックの中 で、無限ループを抜け出す ための 条件式を記述 し、その 条件式が True
になった場合に break 文 を実行して 無限ループから抜け出す というものです。while 文 の 条件式 は、繰り返し を 続ける ための 条件 なので、while 文の 条件式 に繰り返しを 続けるための条件式 を 記述 することで、break 文 を 記述する必要 が なくなります。具体的には、下記のプログラムのように記述します。
- 5 行目:while 文の条件式に、決着がついていない ことを 判定 する 式を記述する
- 元のプログラムにあった、if 文と break 文を削除 する
while 文 の 条件式 は、先程のプログラムの if 文の条件式 の 逆 で、繰り返しを 続けるための条件 を表すので、!=
ではなく ==
を記述 する必要がある点に 注意 して下さい。
1 def play(self):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲームの決着がついていない間繰り返す
5 while self.judge() == Marubatsu.PLAYING:
6 # ゲーム盤の表示
7 print(self)
8 # キーボードからの座標の入力
9 coord = input("x,y の形式で座標を入力して下さい")
10 # x 座標と y 座標の計算
11 x, y = coord.split(",")
12 # (x, y) に着手を行う
13 self.move(int(x), int(y))
14
15 Marubatsu.play = play
行番号のないプログラム
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.judge() == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい")
# x 座標と y 座標の計算
x, y = coord.split(",")
# (x, y) に着手を行う
self.move(int(x), int(y))
Marubatsu.play = play
修正箇所
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
- while True:
+ while self.judge() == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい")
# x 座標と y 座標の計算
x, y = coord.split(",")
# (x, y) に着手を行う
self.move(int(x), int(y))
- if self.judge() != Marubatsu.PLAYING:
- break
Marubatsu.play = play
下記のプログラムを実行し、0,0
、0,1
、1,0
、1,1
、2,0
の順でテキストボックスに入力することで、play
メソッドが正しく動作することを確認できます。実行結果は先ほどと同様なので省略します。
mb.play()
while 文の記述に関する補足
while 文のブロックの中に break を記述して繰り返しを抜けるよりも、上記のように while 文 の 条件式 に繰り返しの 続行を判定する式 を記述したほうがプログラムが わかりやすい ので、一般的 には while 文は 上記のように記述 したほうが 良い でしょう。本記事でも上記の記述方法を採用することにします。
ただし、常に 上記のような記述をすれば良いとは 限りません。例えば、上記のような記述をしてしまうと、while 文を実行した際に、最初から while 文の 条件式が False
になってしまう場合は、一度も while 文の ブロックが実行されません 。そのため、while 文のブロック を最低でも 1 度は実行したい 場合は、while 文の ブロックの最後 に無限ループを 抜けるため の if 文と break 文を記述 する必要があります。他にも、while 文のブロックの 繰り返しを続行 する 条件が複雑 な場合は、while 文の 条件式が長くなる ため、if 文と break 文 を使ったほうが わかりやすい でしょう。言葉だけの説明ではわかりづらいと思いますが、今後の記事で具体的な実例が出た際に、このことを詳しく説明することにします。
play
メソッドの問題点
play
メソッドを実装したことで、座標をテキストボックスに入力するだけで〇×ゲームを遊べるようになりましたが、現状の play
メソッドには いくつかの問題 があるので、次回の記事ではそれらの 問題点を修正 して play
メソッドを 改良 することにします。
次回の記事を見る前に、実際に 何度か play
メソッドを 実行 して〇×ゲームを 遊んでみて、どのような問題があるか を考えておいてください。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
次回の記事