目次と前回の記事
前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤のすべてのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
先手は 〇 のプレイヤーである
プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
前回までのおさらい
前回の記事では、〇×ゲームを遊ぶための play
メソッドを実装しましたが、play
メソッドにはいくつかの問題点があります。今回の記事では、それらの問題点を修正します。
play
メソッドの改良
下記を見る前に、play
メソッドにどのような問題があり、どのような方法で改良できるかについて少し考えてみて下さい。
結果の表示
実際に 決着がつくまで 〇×ゲームを遊ぶとわかると思いますが、play
メソッドでは、決着がついた場合 のゲーム盤が 表示されません。この問題は、下記のプログラムのように、ゲームの決着がついた、while 文の ブロックの後 で ゲーム盤を表示 することで解決できます。
from marubatsu import Marubatsu
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))
# 決着がついたので、ゲーム盤を表示する
print(self)
Marubatsu.play = play
修正箇所
from marubatsu import Marubatsu
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))
# 決着がついたので、ゲーム盤を表示する
+ print(self)
Marubatsu.play = play
下記のプログラムを実行し、0,0
、0,1
、1,0
、1,1
、2,0
の順でテキストボックスに入力することで、決着がついたゲーム盤が表示されることを確認できます。
mb = Marubatsu()
mb.play()
実行結果
略
Turn o
oo.
xx.
...
Turn x
ooo
xx.
...
勝者の表示
上記の改良により、決着がついた際のゲーム盤が表示されるようになりましたが、誰が勝利したか の 表示 が 行われない という問題があります。誰が勝利したか は、judge
メソッドで 判定 でき、下記のプログラムの 16 行目のように、ゲーム盤を表示する前に judge
メソッドの返り値である 判定結果を表示 することで、勝者を表示できます。
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 # 決着がついたので、勝者とゲーム盤を表示する
16 print("winner", self.judge())
17 print(self)
18
19 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))
# 決着がついたので、勝者とゲーム盤を表示する
print("winner", self.judge())
print(self)
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))
# 決着がついたので、勝者とゲーム盤を表示する
+ print("winner", self.judge())
print(self)
Marubatsu.play = play
下記のプログラムを実行し、0,0
、0,1
、1,0
、1,1
、2,0
の順でテキストボックスに入力することで、勝者と、決着がついたゲーム盤が表示されることを確認できます。
mb.play()
実行結果
略
Turn o
oo.
xx.
...
winner o
Turn x
ooo
xx.
...
上記は 〇 の勝利の場合ですが、念のため、× の勝利 と 引き分け の場合も 確認 します。
下記のプログラムを実行し、0,1
、0,0
、1,1
、1,0
、0,2
、2,0
の順でテキストボックスに入力することで、× の勝利が表示されることを確認できます。
mb.play()
実行結果
略
Turn x
xx.
oo.
o..
winner x
Turn o
xxx
oo.
o..
下記のプログラムを実行し、0,0
、0,1
、1,0
、1,1
、2,1
、2,0
、0,2
、1,2
、2,2
の順でテキストボックスに入力することで、引き分けが表示されることを確認できます。
mb.play()
実行結果
略
Turn o
oox
xxo
ox.
winner draw
Turn x
oox
xxo
oxo
実は、Marubatsu.DRAW
に "draw"
という文字列を代入したのは、 上記のように Marubatsu.DRAW
を そのまま表示 することで、引き分け であることを 表示 できるようにするためでした。Marubatsu.DRAW
に 別の文字列 を 代入 した場合は、下記のようなプログラムを記述する必要があります。
if self.judge() == Marubatsu.DRAW:
print("winner draw")
else:
print("winner", self.judge())
Marbatsu.CIRCLE
や Marubatsu.CROSS
に "o"
や "x"
以外の文字列を代入 した場合も 同様 です。
ゲーム盤の表示の改良
上記の実行結果では、決着が付いた状態 で、winner draw
の下に Turn x
のように、次の手番 が × のプレイヤーであることが 表示 されていますが、決着がついているのに 次の手番が 表示 されるのは おかしい と思いませんか?そこで、次は、決着が付いた場合 は、手番を表示しない ように改良することにします。
ゲーム盤の 表示 に関する 処理 は __str__
メソッドで行っているので、この メソッドの中 で、ゲームの 決着がついている場合 は 手番を表示しない ように 修正 することにします。また、そのついでに、勝利者の表示 も __str__
メソッドの中で まとめて行う ように 修正 することにします。そのように修正することで、決着がついた場合 に、print(self)
だけを記述 すれば済むようになります。修正後の __str__
メソッドは以下のようになります。
-
3、4 行目:
judge
メソッドを使って、ゲームの 決着がついていないこと を 判定 し、決着がついていない 場合は、手番を表す文字列 をtext
に代入 する -
6、7 行目:決着がついている 場合は、
judge
メソッドを使って 勝者を表す文字列 をtext
に代入 する1
1 def __str__(self):
2 # ゲームの決着がついていない場合は、手番を表示する
3 if self.judge() == Marubatsu.PLAYING:
4 text = "Turn " + self.turn + "\n"
5 # 決着がついていれば勝者を表示する
6 else:
7 text = "winner " + self.judge() + "\n"
8 for y in range(self.BOARD_SIZE):
9 for x in range(self.BOARD_SIZE):
10 text += self.board[x][y]
11 text += "\n"
12 return text
13
14 Marubatsu.__str__ = __str__
行番号のないプログラム
def __str__(self):
# ゲームの決着がついていない場合は、手番を表示する
if self.judge() == Marubatsu.PLAYING:
text = "Turn " + self.turn + "\n"
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.judge() + "\n"
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
text += self.board[x][y]
text += "\n"
return text
Marubatsu.__str__ = __str__
修正箇所
def __str__(self):
- text = "Turn " + self.turn + "\n"
# ゲームの決着がついていない場合は、手番を表示する
+ if self.judge() == Marubatsu.PLAYING:
+ text = "Turn " + self.turn + "\n"
# 決着がついていれば勝者を表示する
+ else:
+ text = "winner " + self.judge() + "\n"
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
text += self.board[x][y]
text += "\n"
return text
Marubatsu.__str__ = __str__
決着がついた際の表示を __str__
メソッドで記述するように修正したので、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))
# 決着がついたので、ゲーム盤を表示する
print(self)
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))
# 決着がついたので、ゲーム盤を表示する
- print("winner", self.judge())
print(self)
Marubatsu.play = play
下記のプログラムを実行し、0,0
、0,1
、1,0
、1,1
、2,0
の順でテキストボックスに入力することで、決着がついた場面で手番が表示されなくなることが確認できます。
mb.play()
実行結果
略
Turn o
oo.
xx.
...
winner o
ooo
xx.
...
judge
メソッドの呼び出しの抑制
修正したプログラムは 正しく動作 しますが、judge
メソッドを 必要がない のに 何度も呼び出す という、無駄な処理 が行われています。
具体的にどのような点が無駄であるかについて少し考えてみて下さい。
無駄な処理の確認
どのような点が無駄であるかについては、play
メソッドが行う処理を順に 辿っていく ことで 確認 することができます。play
メソッドは、下記のプログラムの 5 行目のように、while 文の 条件式の中 で judge
メソッドを 呼び出し ています。
1 def play(self):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲームの決着がついていない間繰り返す
5 while self.judge() == Marubatsu.PLAYING:
6 # ゲーム盤の表示
7 print(self)
8 以下略
また、その直後の 7 行目で print(self)
が実行されると、__str__
メソッドが 呼び出され ますが、下記のプログラムのように、__str__
メソッドの 3 行目の if 文 の 条件式 でも judge
メソッドが 呼び出され ています。このことから、play
メソッドの 5 行目 と 7 行目 が 実行 されると、必ず judge
メソッドが 2 回呼び出される ことがわかります。
1 def __str__(self):
2 # ゲームの決着がついていない場合は、手番を表示する
3 if self.judge() == Marubatsu.PLAYING:
4 text = "Turn " + self.turn + "\n"
5 # 決着がついていれば勝者を表示する
6 else:
7 text = "winner " + self.judge() + "\n"
8 以下略
長くなるので再掲はしませんが、judge
メソッドの ブロックの中 では、勝敗判定 を行うための、ある程度 複雑な処理 が行われますが、judge
メソッドの 返り値 は、ゲーム盤の状況 が 同じ であれば、常に同じ結果 が計算されます。play
メソッドの 5 行目 と 7 行目 を実行する際に、着手 は 一度も行われない ので、どちらも ゲーム盤の状況 は 同じ です。従って、5 行目 と、7 行目 で judge
メソッドを 呼び出し て判定を行うのは、結果が変わらない 複雑な 作業 を 2 度行う という、無駄な作業 を行っていることになります。
__str__
メソッドの中にも、3 行目 の 条件式 の計算結果が False
になる場合に、7 行目 で もう一度 judge
メソッドが 実行 されるという、同様の無駄 があります。
属性の追加
このような 無駄な作業 を行う必要が 生じる理由 は、judge
メソッドの 返り値 がどこにも 記録されていない からです。judge
メソッドの 返り値 を 記録 するための 属性 を用意し、そこに judge
メソッドの 返り値を代入 することで、勝敗判定の結果 が 必要になった時 に、何度も judge
メソッドを 呼び出す必要 が なくなります。
プログラムに 新しい変数 や 属性 を 追加 する場合は、その変数や属性の 名前、初期化処理、更新処理、利用方法 について 考える 必要があります。
属性の名前
test_judge
では、winner
という名前の変数に期待される judge
メソッドの返り値を代入していましたが、judge
メソッドの返り値には、勝者(winner)とは 関係のない、Marubatsu.PLAYING
がある ので、winner
という名前は ふさわしくありません。judge
メソッドの 返り値 は、ゲームの状態(status)を表すので、status
という名前にします。
初期化処理
ゲームの 開始時 は、ゲームの状態 は Marubatsu.PLAYING
なので、下記のプログラムの 5 行目のように、再起動 を行う restart
メソッドの中 で 初期化処理 を行います。
1 def restart(self):
2 self.initialize_board()
3 self.turn = Marubatsu.CIRCLE
4 self.move_count = 0
5 self.status = Marubatsu.PLAYING
6
7 Marubatsu.restart = restart
行番号のないプログラム
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
Marubatsu.restart = restart
修正箇所
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
+ self.status = Marubatsu.PLAYING
Marubatsu.restart = restart
更新処理
status
属性には、judge
メソッドの 返り値を代入 する必要があるので、judge
メソッドの 呼び出し を行う部分を self.status = self.judge()
のように修正します。
ただし、プログラムに 記述 されている すべて の judge
メソッドの 呼び出し をそのように 修正 しても、judge
メソッドが 呼び出される ことに 変わりはない ので、judge
メソッドの 無駄な呼び出し は 解消されません。judge
メソッドの 無駄な呼び出し を 解消 するためには、どの judge
メソッドの 呼び出し が 必要であるか について考える必要があります。
〇×ゲームの、勝敗判定 の 結果が変わる のは、ゲーム盤の 状況が変わった時だけ で、ゲーム盤の 状況が変わる のは move
メソッドによって 着手を行った場合だけ です。従って、judge
メソッドを 実行 する 必要がある のは、move
メソッドで 着手 が行われた 場合だけ なので、下記のプログラムの 5 行目のように、move
メソッドで 着手 が行われた場合に judge
メソッドを呼び出して 勝敗判定 を行い、その 返り値 を status
属性 に 代入 するように修正します。
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
3 self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
4 self.move_count += 1
5 self.status = self.judge()
6
7 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
Marubatsu.move = move
修正箇所
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
+ self.status = self.judge()
Marubatsu.move = move
利用
status
属性 の 利用場面 は、これまで judge
メソッドの 呼び出しを記述 していた部分なので、それらの self.judge()
を self.status
に 置きかえます。具体的には、下記のプログラムのように __str__
メソッドの 3、7 行目を修正します。
1 def __str__(self):
2 # ゲームの決着がついていない場合は、手番を表示する
3 if self.status == Marubatsu.PLAYING:
4 text = "Turn " + self.turn + "\n"
5 # 決着がついていれば勝者を表示する
6 else:
7 text = "winner " + self.status + "\n"
8 for y in range(self.BOARD_SIZE):
9 for x in range(self.BOARD_SIZE):
10 text += self.board[x][y]
11 text += "\n"
12 return text
13
14 Marubatsu.__str__ = __str__
行番号のないプログラム
def __str__(self):
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn + "\n"
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status + "\n"
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
text += self.board[x][y]
text += "\n"
return text
Marubatsu.__str__ = __str__
修正箇所
def __str__(self):
# ゲームの決着がついていない場合は、手番を表示する
- if self.judge() == Marubatsu.PLAYING:
+ if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn + "\n"
# 決着がついていれば勝者を表示する
else:
- text = "winner " + self.judge() + "\n"
+ text = "winner " + self.status + "\n"
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
text += self.board[x][y]
text += "\n"
return text
Marubatsu.__str__ = __str__
次に play
メソッドの 5 行目を下記のプログラムのように修正します。
1 def play(self):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲームの決着がついていない間繰り返す
5 while self.status == 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 # 決着がついたので、ゲーム盤を表示する
16 print(self)
17
18 Marubatsu.play = play
行番号のないプログラム
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい")
# x 座標と y 座標の計算
x, y = coord.split(",")
# (x, y) に着手を行う
self.move(int(x), int(y))
# 決着がついたので、ゲーム盤を表示する
print(self)
Marubatsu.play = play
修正箇所
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
- while self.judge() == Marubatsu.PLAYING:
+ while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい")
# x 座標と y 座標の計算
x, y = coord.split(",")
# (x, y) に着手を行う
self.move(int(x), int(y))
# 決着がついたので、ゲーム盤を表示する
print(self)
Marubatsu.play = play
下記のプログラムを実行し、0,0
、0,1
、1,0
、1,1
、2,0
の順でテキストボックスに入力することで、修正したプログラムが正しく動作することが確認できます。実行結果は先ほどと同様なので省略します。
mb.play()
間違った入力への対処
何度か play
メソッドを実行して〇×ゲームを遊べばわかると思いますが、座標の入力 を 間違う と、このプログラムは エラーが発生 してプログラムの 処理が止まって しまいます。
たとえば、下記のプログラムを実行し、テキストボックスに 1,1
を入力したつもりが、,
を書き忘れ て 11
を入力すると、下記のような エラーが発生 します。
mb.play()
実行結果
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\038\marubatsu.ipynb セル 22 line 1
----> 1 mb.play()
c:\Users\ys\ai\marubatsu\038\marubatsu.ipynb セル 22 line 1
9 coord = input("x,y の形式で座標を入力して下さい")
10 # x 座標と y 座標の計算
---> 11 x, y = coord.split(",")
12 # (x, y) に着手を行う
13 self.move(int(x), int(y))
ValueError: not enough values to unpack (expected 2, got 1)
上記のエラーメッセージは、以下のような意味を持ちます。
-
ValueError
値(value)に関するエラー -
not enough values to unpack (expected 2, got 0)
展開する(to unpack)ために必要な値(value)が 2 つ期待されている(expected)が、0 個(got 0)しかないため足りない(not enough)
エラーの原因
エラーの 原因 は、"11"
という、中に ","
が存在しない 文字列に対して、split(",")
を実行すると、下記のプログラムのように、["11"]
という、要素が 1 つしかない list が返されてしまうからです。x, y = coord.split(",")
のような、list の展開 を記述した場合、代入 する 変数の数 と list の 素の数 が 一致しない と エラーが発生 してしまいます。
print("11".split(","))
実行結果
['11']
input
のように、ユーザの入力 に 対応した処理 を行うようなプログラムを記述する際に、上記のような、意図しない ような 入力 が行われることを 考慮する必要 があります。このことを 考慮せず にプログラムを 記述 してしまうと、上記のような エラーが発生 したり、エラーは発生しないが、プログラムの挙動 が おかしくなってしまう という バグが発生 するからです。プログラムの初心者のうちは、そのことに気が回らないかもしれませんが、意図しない入力 による バグ は 良く発生する バグなので、意識する ように 心がけて 下さい。
バグの修正
上記のエラーは、coord.split(",")
によって返された list の要素 が 2 つ以外 の場合に発生するので、下記のプログラムのように、返された list の要素 が 2 つ以外 の場合は エラーメッセージを表示 して、着手の処理 を 行わない ようにすることで修正できます。
-
11 行目:
coord.split(",")
の 計算結果 をxylist
に代入 する -
13 ~ 17 行目:
xylist
の 要素の数 が 2 でない 場合、15 行目で エラーメッセージを表示 し、17 行目で continue 文 を 実行 することで、残り の while 文の ブロック を 実行せず に、次の繰り返し処理 を 行う -
18 行目:
xylist
の 要素の数 が 2 である ことが 確定 したので、xylist
のそれぞれの 要素 をx
とy
に展開 する
1 def play(self):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲームの決着がついていない間繰り返す
5 while self.status == Marubatsu.PLAYING:
6 # ゲーム盤の表示
7 print(self)
8 # キーボードからの座標の入力
9 coord = input("x,y の形式で座標を入力して下さい")
10 # x 座標と y 座標を要素として持つ list を計算する
11 xylist = coord.split(",")
12 # xylist の要素の数が 2 ではない場合
13 if len(xylist) != 2:
14 # エラーメッセージを表示する
15 print("x, y の形式ではありません")
16 # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
17 continue
18 x, y = xylist
19 # (x, y) に着手を行う
20 self.move(int(x), int(y))
21
22 # 決着がついたので、ゲーム盤を表示する
23 print(self)
24
25 Marubatsu.play = play
行番号のないプログラム
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい")
# x 座標と y 座標を要素として持つ list を計算する
xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
if len(xylist) != 2:
# エラーメッセージを表示する
print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
continue
x, y = xylist
# (x, y) に着手を行う
self.move(int(x), int(y))
# 決着がついたので、ゲーム盤を表示する
print(self)
Marubatsu.play = play
修正箇所
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい")
# x 座標と y 座標を要素として持つ list を計算する
- x, y = coord.split(",")
+ xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
+ if len(xylist) != 2:
# エラーメッセージを表示する
+ print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
+ continue
+ x, y = xylist
# (x, y) に着手を行う
self.move(int(x), int(y))
# 決着がついたので、ゲーム盤を表示する
print(self)
Marubatsu.play = play
下記のプログラムを実行し、テキストボックスに 11
を入力することで、実行結果のように エラーメッセージが表示 され、着手を行わず に ゲームが続行 されることが確認できます。
mb.play()
実行結果
Turn o
...
...
...
x, y の形式ではありません
Turn o
...
...
...
ゲームの強制終了
上記は 11
という入力に対して 意図した処理 が行われるかどうかを 確認 するために行った作業ですが、11
を 入力した後 も ゲームは続行 されます。そのため、ゲームを終了 させるためには、0,0
、0,1
、1,0
、1,1
、2,0
などの順で テキストボックスに入力 して実際にゲームを終了させる必要があります。これは 面倒 だと思いませんか?
VSCode の JupyterLab では、タイトルバーの下 にある、下図の 割り込みボタン を クリック することで、プログラムの 実行途中 でプログラムを 強制的に終了 させることができます。ただし、このボタンをクリックしても なかなか プログラムが 終了しなかったり、再起動を促すパネルが表示 されるようなことがあるので、〇×ゲームの場合は、別の方法 でゲームを 簡単に終了 させる 方法を用意 しておいたほうが便利です。
そのような方法の一つに、テキストボックス に 特定の文字列を入力 することで、〇×ゲームを 終了させる という方法があります。例えば、下記は、exit
という文字列を 入力 すると ゲームが終了する ように play
メソッドを 修正 したプログラムです。もちろん、他の文字列を入力することでゲームが終了するように変更してもかまいません。
-
9 行目:
exit
を入力すると終了するこをと 説明するメッセージを追加 する -
11 ~ 13 行目:テキストボックスに 入力した文字列 が
"exit"
と 等しいかどうか を 判定 し、等しければ 12 行目で ゲームの終了メッセージを表示 し、12 行目で return 文 を実行することで、play
メソッドの 処理を終了 する
1 def play(self):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲームの決着がついていない間繰り返す
5 while self.status == Marubatsu.PLAYING:
6 # ゲーム盤の表示
7 print(self)
8 # キーボードからの座標の入力
9 coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
10 # "exit" が入力されていればメッセージを表示して関数を終了する
11 if coord == "exit":
12 print("ゲームを終了します")
13 return
14 # x 座標と y 座標を要素として持つ list を計算する
15 xylist = coord.split(",")
16 # xylist の要素の数が 2 ではない場合
17 if len(xylist) != 2:
18 # エラーメッセージを表示する
19 print("x, y の形式ではありません")
20 # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
21 continue
22 x, y = xylist
23 # (x, y) に着手を行う
24 self.move(int(x), int(y))
25
26 # 決着がついたので、ゲーム盤を表示する
27 print(self)
28
29 Marubatsu.play = play
行番号のないプログラム
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
# "exit" が入力されていればメッセージを表示して関数を終了する
if coord == "exit":
print("ゲームを終了します")
return
# x 座標と y 座標を要素として持つ list を計算する
xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
if len(xylist) != 2:
# エラーメッセージを表示する
print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
continue
x, y = xylist
# (x, y) に着手を行う
self.move(int(x), int(y))
# 決着がついたので、ゲーム盤を表示する
print(self)
Marubatsu.play = play
修正箇所
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
- coord = input("x,y の形式で座標を入力して下さい")
+ coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
# "exit" が入力されていればメッセージを表示して関数を終了する
+ if coord == "exit":
+ print("ゲームを終了します")
+ return
# x 座標と y 座標を要素として持つ list を計算する
xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
if len(xylist) != 2:
# エラーメッセージを表示する
print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
continue
x, y = xylist
# (x, y) に着手を行う
self.move(int(x), int(y))
# 決着がついたので、ゲーム盤を表示する
print(self)
Marubatsu.play = play
下記のプログラムを実行し、exit
をテキストボックスに入力することで、メッセージを表示してゲームが終了することが確認できます。
mb.play()
実行結果
Turn o
...
...
...
ゲームを終了します
ゲーム盤の外の座標の入力への対処
先程の修正で、11
のような入力に対応する処理を記述できましたが、5,1
のような、〇×ゲームの ゲーム盤 に 存在しない座標 を入力すると エラーが発生 します。
mb.play()
実行結果
略
File c:\Users\ys\ai\marubatsu\038\marubatsu.py:74, in Marubatsu.place_mark(self, x, y, mark)
55 def place_mark(self, x: int, y: int, mark: str):
56 """ ゲーム盤の指定したマスに指定したマークを配置する.
57
58 (x, y) のマスに mark で指定したマークを配置する.
(...)
71 マークを配置できた場合は True、配置できなかった場合は False
72 """
---> 74 if self.board[x][y] == Marubatsu.EMPTY:
75 self.board[x][y] = mark
76 return True
IndexError: list index out of range
上記のエラーメッセージは、以下のような意味を持ちます。
-
IndexError
インデックス(index)に関するエラー -
list index out of range
list のインデックス(index)が範囲外(out of range)である
このエラーは、エラーメッセージの --->
の部分の if self.board[x][y]
を計算する際に、x
に 5
が代入 されているため、対応するインデックス が 存在しない self.board
の 要素 を 参照 しようとしたことが原因です。〇×ゲームの ゲーム盤のサイズ は 3 x 3 なので、x 座標 と y 座標 はいずれも 0 以上 3 未満 の 整数 である必要があります。
現状の move
メソッドでは、2 行目で place_mark
を呼び出す ことで 着手 を行っています。
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
また、place_mark
メソッドでは、すでにマークが配置されている座標 に対して マークを配置 しようとした場合は、エラーメッセージを表示 して 配置を行わない ようにしていますが、範囲外の座標 に 対する処理 は 記述していません。そこで、下記のプログラムのように、ゲーム盤の 範囲外の座標 に対して マークを配置 しようとした場合に、エラーメッセージを表示 して 配置を行わない ように修正します。
-
2 ~ 4 行目:(x, y) がゲーム盤の 範囲外の座標 である 条件 は、
x
またはy
が 0 未満 または ゲーム盤のサイズであるself.BOARD_SIZE
以上 の場合なので、その 4 つの条件 を表す 条件式 を or 演算子 で連結することで、ゲーム盤の 範囲外 であることを 判定 する。範囲外と判定 された場合は、3 行目で エラーメッセージを表示 し、4 行目で 配置できなかった ことを表すFalse
を返す - 5 行目:2 ~ 4 行目に if 文を記述 したので、元の if を elif に修正 した
1 def place_mark(self, x, y, mark):
2 if x < 0 or x >= self.BOARD_SIZE or y < 0 or y > self.BOARD_SIZE:
3 print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
4 return False
5 elif self.board[x][y] == Marubatsu.EMPTY:
6 self.board[x][y] = mark
7 return True
8 else:
9 print("(", x, ",", y, ") のマスにはマークが配置済です")
10 return False
11
12 Marubatsu.place_mark = place_mark
行番号のないプログラム
def place_mark(self, x, y, mark):
if x < 0 or x >= self.BOARD_SIZE or y < 0 or y > self.BOARD_SIZE:
print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
return False
elif self.board[x][y] == Marubatsu.EMPTY:
self.board[x][y] = mark
return True
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
return False
Marubatsu.place_mark = place_mark
修正箇所
def place_mark(self, x, y, mark):
+ if x < 0 or x >= self.BOARD_SIZE or y < 0 or y > self.BOARD_SIZE:
+ print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
+ return False
- if self.board[x][y] == Marubatsu.EMPTY:
+ elif self.board[x][y] == Marubatsu.EMPTY:
self.board[x][y] = mark
return True
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
return False
Marubatsu.place_mark = place_mark
下記のプログラムを実行し、5,1
をテキストボックスに入力することで、エラーメッセージが表示されて、マークが配置されずにゲームが続行することが確認できます。なお、実行結果には表示していませんが、確認後に exit
を入力してゲームを終了させました。
mb.play()
実行結果
Turn o
...
...
...
( 5 , 1 ) はゲーム盤の範囲外の座標です
Turn o
...
...
...
if 文の判定の順番に関する注意点
place_mark
で行う if 文の判定 を、下記のプログラムのように、先に (x, y) のマスに マークが配置されていない ことを 判定 し、その後 で (x, y) が ゲーム盤の範囲外 であることを 判定 するように記述しても良いのではないかと 思う人がいるかも しれません。
def place_mark(self, x, y, mark):
if self.board[x][y] == Marubatsu.EMPTY:
self.board[x][y] = mark
return True
elif x < 0 or x >= self.BOARD_SIZE or y < 0 or y > self.BOARD_SIZE:
print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
return False
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
return False
Marubatsu.place_mark = place_mark
実際に上記のように place_mark
を修正し、下記のプログラムを実行して 5,1
を実行すると、下記のように、先程と同じエラー が発生します。
mb.play()
実行結果
略
c:\Users\ys\ai\marubatsu\038\marubatsu.ipynb セル 37 line 2
1 def place_mark(self, x, y, mark):
----> 2 if self.board[x][y] == Marubatsu.EMPTY:
3 self.board[x][y] = mark
4 return True
IndexError: list index out of range
このエラーが発生する理由は、先に if self.board[x][y] == Marubatsu.EMPTY
を 判定 するようにしたことで、x
に 5
が代入 された状態で この式の計算が行われる ためです。
このように、if 文などの 条件式 の 記述の順番 が、処理の結果 に 大きな影響を及ぼす 場合があるので、if 文などを記述する際には、その点に 注意しながら記述する必要 があります。
別の記述方法
先程は、(x, y) が ゲーム盤の外 にあることを or 演算子 を使って 判定 しましたが、下記のプログラムのように、and 演算子 を使って (x, y) が ゲーム盤の中 にあることを 判定 するように place_mark
を記述することもできます。ただし、この場合は修正前のように 1 つ の if 文 ではなく、(x, y) がゲーム盤の中にあることが判定された場合の ブロックの中 に、別の if 文 を 入れ子 で記述する必要がある点に注意して下さい。
-
2 行目:(x, y) がゲーム盤の 範囲内の座標 である 条件 は、
x
とy
の 両方 が 0 以上 ゲーム盤のサイズであるself.BOARD_SIZE
未満 の場合なので、その 4 つの条件 を表す 条件式 を and 演算子 で連結することで、ゲーム盤の 範囲外 であることを 判定 する。 -
3 ~ 8 行目:範囲内と判定 された場合は、元の
place_mark
で行っていた 処理を行う -
9 ~ 11 行目:範囲外と判定 された場合は、エラーメッセージを表示 し、配置できなかった ことを表す
False
を返す
1 def place_mark(self, x, y, mark):
2 if x >= 0 and x < self.BOARD_SIZE and y >= 0 and y < self.BOARD_SIZE:
3 if self.board[x][y] == Marubatsu.EMPTY:
4 self.board[x][y] = mark
5 return True
6 else:
7 print("(", x, ",", y, ") のマスにはマークが配置済です")
8 return False
9 else:
10 print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
11 return False
12
13 Marubatsu.place_mark = place_mark
行番号のないプログラム
def place_mark(self, x, y, mark):
if x >= 0 and x < self.BOARD_SIZE and y >= 0 and y < self.BOARD_SIZE:
if self.board[x][y] == Marubatsu.EMPTY:
self.board[x][y] = mark
return True
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
return False
else:
print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
return False
Marubatsu.place_mark = place_mark
なお、修正箇所は、かなりわかりづらかったので省略します。
下記のプログラムを実行し、5,1
をテキストボックスに入力することで、エラーメッセージが表示されて、マークが配置されずにゲームが続行することが確認できます。実行結果は先ほどと同じなので省略します。
mb.play()
上記のプログラムの 2 行目の if 文は、以前の記事で紹介した、比較演算子の連鎖 を使って、下記のように簡潔に記述することができます。
if 0 <= x < self.BOARD_SIZE and 0 <= y < self.BOARD_SIZE:
修正箇所
- if x >= 0 and x < self.BOARD_SIZE and y >= 0 and y < self.BOARD_SIZE:
+ if 0 <= x < self.BOARD_SIZE and 0 <= y < self.BOARD_SIZE:
上記のように place_mark
を修正し、下記のプログラムを実行し、5,1
をテキストボックスに入力することで、エラーメッセージが表示されて、マークが配置されずにゲームが続行することが確認できます。実行結果は先ほどと同じなので省略します。
mb.play()
こちらの方がわかりやすいので、本記事でもこちらを採用することにします。
座標に整数以外の文字を入力した場合の処理
上記の修正で、座標の入力に関するバグがすべて修正されたと思った人がいるかもしれませんが、実際には もう一つバグ が残っています。それは、a,b
のような、整数以外 の座標を入力した場合で、下記のプログラムを実行して a,b
を入力すると エラーが発生 します。
mb.play()
実行結果
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\038\marubatsu.ipynb セル 44 line 1
----> 1 mb.play()
c:\Users\ys\ai\marubatsu\038\marubatsu.ipynb セル 44 line 2
22 x, y = xylist
23 # (x, y) に着手を行う
---> 24 self.move(int(x), int(y))
26 # 決着がついたので、ゲーム盤を表示する
27 print(self)
ValueError: invalid literal for int() with base 10: 'a'
上記のエラーメッセージは、以下のような意味を持ちます。
-
ValueError
値(Value)に関するエラー -
invalid syntax
"a" は、10 を基数(base)とする(10 進数の事)数値型としては不正(invalid)なリテラル(literal)である
このエラーは、x
に "a"
という、整数に変換 することが 不可能な文字列 が代入された状態で、x
を int
を使って 整数型 に 変換しようとしたこと が原因です。
このエラーが発生しないようにする方法の一つに、x
と y
に 整数に変換できない ような文字列が代入されていた場合に、int(x)
と int(y)
を 実行しないようにする という方法がありますが、文字列 を 整数に変換できるかどうか を 判定 することは 簡単ではありません。
例外処理
別の方法として、エラーが発生した場合 に、プログラムの 処理 を 停止せず に、別の処理を行う、例外処理 という方法があります。これまで説明していませんでしたが、プログラムの 処理が停止 するような エラー のことを 例外(exception)と呼び、例外処理 では 例外が発生した際 に 行う処理 を下記のように記述します。
try:
プログラム
except:
try のブロックで例外が発生した時に行う処理
例外処理では、以下のような処理を行います。
- try のブロック のプログラムを 実行した際 に 例外(エラー)が発生 すると、その時点で try のブロック の 処理 を 即座に中断 し、except のブロック を 実行 する。その際に、本来表示されるはずの エラーメッセージ は 表示されず、プログラム は 停止しない
- try のブロック のプログラムを実行した際に エラーが発生しなかった場合 は、except のブロック は 実行されない
例外処理についての詳細については下記のリンク先を参照して下さい
self.move(int(x), int(y))
を、下記のプログラムのように 例外処理で記述 することで、int(x)
と int(y)
を 実行 した際に エラーが発生 した場合に、プログラムが 停止せず に、except のブロック に記述した エラーメッセージを表示 するプログラムが 実行 されます。従って、エラーが発生 した際に move
メソッドは 実行されない ので、着手が行われたり、手番が変わったり、status
属性が変化したりすることはありません。
try:
self.move(int(x), int(y))
except:
print("整数の座標を入力して下さい")
上記のプログラムを実行する際に x
に "a"
が代入 されていた場合は、int(x)
を 実行した時点 で 例外が発生 し、その時点で try のブロック の 処理が中断 されて即座に except
のブロックが実行されるので、move
メソッドが 実行される ことは ありません。
下記は play
メソッドの 24 ~ 27 行目を上記のように修正したプログラムです。
1 def play(self):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲームの決着がついていない間繰り返す
5 while self.status == Marubatsu.PLAYING:
6 # ゲーム盤の表示
7 print(self)
8 # キーボードからの座標の入力
9 coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
10 # "exit" が入力されていればメッセージを表示して関数を終了する
11 if coord == "exit":
12 print("ゲームを終了します")
13 return
14 # x 座標と y 座標を要素として持つ list を計算する
15 xylist = coord.split(",")
16 # xylist の要素の数が 2 ではない場合
17 if len(xylist) != 2:
18 # エラーメッセージを表示する
19 print("x, y の形式ではありません")
20 # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
21 continue
22 x, y = xylist
23 # (x, y) に着手を行う
24 try:
25 self.move(int(x), int(y))
26 except:
27 print("整数の座標を入力して下さい")
28
29 # 決着がついたので、ゲーム盤を表示する
30 print(self)
31
32 Marubatsu.play = play
行番号のないプログラム
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
# "exit" が入力されていればメッセージを表示して関数を終了する
if coord == "exit":
print("ゲームを終了します")
return
# x 座標と y 座標を要素として持つ list を計算する
xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
if len(xylist) != 2:
# エラーメッセージを表示する
print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
continue
x, y = xylist
# (x, y) に着手を行う
try:
self.move(int(x), int(y))
except:
print("整数の座標を入力して下さい")
# 決着がついたので、ゲーム盤を表示する
print(self)
Marubatsu.play = play
修正箇所
def play(self):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
# "exit" が入力されていればメッセージを表示して関数を終了する
if coord == "exit":
print("ゲームを終了します")
return
# x 座標と y 座標を要素として持つ list を計算する
xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
if len(xylist) != 2:
# エラーメッセージを表示する
print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
continue
x, y = xylist
# (x, y) に着手を行う
- self.move(int(x), int(y))
+ try:
+ self.move(int(x), int(y))
+ except:
+ print("整数の座標を入力して下さい")
# 決着がついたので、ゲーム盤を表示する
print(self)
Marubatsu.play = play
下記のプログラムを実行して a,b
を実行すると、下記のようなエラーメッセージが表示され、プログラムが停止せずに〇×ゲームが続行することが確認できます。
mb.play()
実行結果
Turn o
...
...
...
整数の座標を入力して下さい
Turn o
...
...
...
input
でキーボードから 数値を入力 させて 処理を行いたい 場合は、数値以外 のデータが 入力 されたときに エラーが発生 して プログラムが停止しない ようにする必要がある。その 方法 の一つに、例外処理 がある。
例外処理は 便利 ですが、気をつけなければならない点 がいくつか あります。また、今回の記事で説明していない 重要な機能 がいくつかありますが、それらを説明するとかなり長くなるので、それらは必要になった時点で説明することにします。
直前に着手を行ったマークの表示
play
メソッドでは、着手を行うたび に ゲーム盤が表示 されますが、その際に、どのマスに着手が行われたか が わかるように表示 を行ったほうが わかりやすい と思いませんか?そこで、直前に行われた着手 を 区別できる ような 表示方法 について考えてみて下さい。
現状のプログラムでは、〇 のマーク を 半角の小文字の o
(オー)、× のマーク を 半角の小文字の "x"(エックス)のように、半角の小文字 の アルファベット で 表示 しています。この 性質を利用 して、直前 に行われた 着手 を表す マーク を、半角の 大文字で表示 するという方法が考えらるので、本記事ではその方法を紹介します。他にもさまざまな方法が考えられるので、良い方法を思いついた方は、実際に実装してみて下さい。
upper
メソッドによる大文字への変換
Python の 文字列型 のデータには、アルファベット を 大文字に変換 した 新しい文字列 を 返す upper
2 というメソッドがあるので、大文字への変換 は、それを利用します。
下記は、upper
メソッドを使って大文字に変換するプログラムです。upper
は アルファベット以外 の文字は 変換しません。また、upper
は 新しい文字列を作成 して返すメソッドなので、3 行目の実行結果のように 元の文字列 は 変化しない 点に注して下さい。
txt = "abc1+?"
print(txt.upper())
print(txt)
実行結果
ABC1+?
abc1+?
upper
メソッドの詳細については、下記のリンクを参照して下さい。
直前の着手を記録する属性
直前 に行われた 着手 を 大文字で表示 するためには、直前 に行われた 着手 を 属性 に 記憶 しておく 必要 があります。プログラムに 新しい属性 を 追加 するので、その属性の 名前、初期化処理、更新処理、利用方法 について 考える ことにします。
属性の名前:直前(last)の 着手(move)を記憶するので、last_move
という名前にする
初期化処理:ゲームの開始時 は、直前 の 着手 が 存在しない ので、下記のプログラムのように、restart
メソッドの中の 6 行目で None
を代入 して初期化する
1 def restart(self):
2 self.initialize_board()
3 self.turn = Marubatsu.CIRCLE
4 self.move_count = 0
5 self.status = Marubatsu.PLAYING
6 self.last_move = None
7
8 Marubatsu.restart = restart
行番号のないプログラム
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = None
Marubatsu.restart = restart
修正箇所
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
+ self.last_move = None
Marubatsu.restart = restart
更新処理:last_move
は着手を行う際に更新されるので、下記のプログラムのように、move
メソッドの中の 6 行目で着手した座標を表す x, y
という tuple を代入3 して更新する
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
3 self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
4 self.move_count += 1
5 self.status = self.judge()
6 self.last_move = x, y
7
8 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
Marubatsu.move = move
修正箇所
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
+ self.last_move = x, y
Marubatsu.move = move
利用方法:last_move
はゲーム盤の表示で利用するので、__str__
メソッドを下記のプログラムのように修正する
-
10、11 行目:(x, y) のマスに 直前の着手が行われている条件 は、「
self.last_move
にNone
が代入されていない」、「self.last_move[0]
とx
が等しい」、「self.last_move[1]
とy
が等しい」の 3 つの条件が すべて満たされた場合 である。10 行目で その条件を判定 し、満たされている場合 は、11 行目でupper
メソッドを使って 大文字に変換 して 表示 する - 12、13 行目:上記の条件が 満たされていない 場合は、直前の着手が行われたマスではないので、これまでと同じ方法で表示 する
1 def __str__(self):
2 # ゲームの決着がついていない場合は、手番を表示する
3 if self.status == Marubatsu.PLAYING:
4 text = "Turn " + self.turn + "\n"
5 # 決着がついていれば勝者を表示する
6 else:
7 text = "winner " + self.status + "\n"
8 for y in range(self.BOARD_SIZE):
9 for x in range(self.BOARD_SIZE):
10 if self.last_move is not None and x == self.last_move[0] and y == self.last_move[1]:
11 text += self.board[x][y].upper()
12 else:
13 text += self.board[x][y]
14 text += "\n"
15 return text
16
17 Marubatsu.__str__ = __str__
行番号のないプログラム
def __str__(self):
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn + "\n"
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status + "\n"
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
if self.last_move is not None and x == self.last_move[0] and y == self.last_move[1]:
text += self.board[x][y].upper()
else:
text += self.board[x][y]
text += "\n"
return text
Marubatsu.__str__ = __str__
修正箇所
def __str__(self):
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn + "\n"
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status + "\n"
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
- text += self.board[x][y]
+ if self.last_move is not None and x == self.last_move[0] and y == self.last_move[1]:
+ text += self.board[x][y].upper()
+ else:
+ text += self.board[x][y]
text += "\n"
return text
Marubatsu.__str__ = __str__
下記のプログラムを実行し、1,1
、0,0
、0,1
をテキストボックスに入力することで、直前の着手が大文字で表示されることが確認できます。
mb.play()
実行結果
Turn o
...
...
...
Turn x
...
.O.
...
Turn o
X..
.o.
...
Turn x
x..
Oo.
...
and 演算子と短絡評価に関する注意点
__str__
メソッドでは、複数の式 を and
演算子 で 連結 した条件式を記述しました。
if self.last_move is not None and x == self.last_move[0] and y == self.last_move[1]:
この条件式の 順番 を、下記のように 変更 すると エラーが発生 するようになります。
if x == self.last_move[0] and self.last_move is not None and y == self.last_move[1]:
エラーが発生する 理由 は、self.last_move
に None
が代入 されている場合に上記の条件式を実行すると、最初 の x == self.last_move[0]
の計算を行う際に、x == None[0]
が計算されることになり、None
は list ではない ので、None[0]
が計算できない からです。
元 のプログラムの 条件式の中 にも 同じ x == self.last_move[0]
が 記述されている ので、エラーが発生すると 思う人がいる かもしれませんが、以前の記事で説明した、and 演算子の短絡評価 によって、以下の手順で計算が行われるため エラーは発生しません。
-
self.last_move
にNone
が代入されている場合 は、最初のself.last_move is not None
がFalse
になる -
and 演算子のみ で 連結 された 条件式 は、連結された 式の計算結果 が 1 つでも
False
になった時点 で、全体の計算結果 がFalse
であることが 確定 する - そのため、
self.last_move is not None
がFalse
になった時点で、残りの式 の 計算 が 行われない ので、エラーは発生しない
and 演算子のみ で 連結 された 条件式 の中で、計算結果が False
になった場合 に、残りの式 が 計算されないようにする ことを目的とした 式 がある場合は、その式 は 先頭に記述 する 必要 がある。なお、これは or 演算子の場合も同様 である。
last_move
の初期化処理の工夫
先程のプログラムでは、last_move
属性の 初期化処理 で、None
を代入 していましたが、move
メソッドで 着手 を行った場合に last_move
に 代入される値 は、1, 2
のような tuple です。このように、last_move
に代入される データ型 が、状況によって異なる と、last_move
を 利用 した処理を行う __str__
メソッドの中で、self.last_move is not None
のような 条件式を記述 して、データ型ごとの処理 を 記述 する必要が生じます。
そこで、last_move
属性に 代入 する データ型を統一する という工夫が考えられます。None
のほうに統一することはできないので、tuple に統一 する必要がありますが、その場合に初期化処理で last_move
にどのような値を代入すればよいかについて考えてみて下さい。
last_move
は、最後の着手の座標 を表すデータで、__str__
メソッドの中で、last_move
と 表示する (x, y) の 座標が等しい場合 にマークを大文字で表示する際に 利用 されます。ゲーム開始時点 では、着手は行われていない ので、__str__
メソッドの中で マークを大文字で表示 する処理を 行ってはいけません。そこで、初期化処理 で last_move
に〇×ゲームのゲーム盤に 存在しない座標 を 代入 しておくことで、ゲーム開始時 でマークを 大文字で表示する処理 が 実行 されることが なくなります。
初期化処理 で last_move
に 代入する座標 は、ゲーム盤に 存在しない座標 であれば なんでもかまいません。そこで、本記事では、下記のプログラムの restart
メソッドの 6 行目のように -1, -1
という座標のデータを 初期化処理で last_move
に代入することにします。
1 def restart(self):
2 self.initialize_board()
3 self.turn = Marubatsu.CIRCLE
4 self.move_count = 0
5 self.status = Marubatsu.PLAYING
6 self.last_move = -1, -1
7
8 Marubatsu.restart = restart
行番号のないプログラム
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
Marubatsu.restart = restart
修正箇所
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
- self.last_move = None
+ self.last_move = -1, -1
Marubatsu.restart = restart
last_move
に常に 要素が 2 つ ある tuple が代入 されるようになったので、str メソッドを下記のプログラムのようにより 簡潔に記述 することができるようになります。
-
10 行目:
self.last_move
には、必ず 要素が 2 つ ある tuple が代入 されるので、tuple の展開 を使ってlastx
とlasty
に 最後の着手の座標 を 代入 する - 11 行目:(x, y) と (lastx, lasty) が 等しいかどうか を 判定 する
1 def __str__(self):
2 # ゲームの決着がついていない場合は、手番を表示する
3 if self.status == Marubatsu.PLAYING:
4 text = "Turn " + self.turn + "\n"
5 # 決着がついていれば勝者を表示する
6 else:
7 text = "winner " + self.status + "\n"
8 for y in range(self.BOARD_SIZE):
9 for x in range(self.BOARD_SIZE):
10 lastx, lasty = self.last_move
11 if x == lastx and y == lasty:
12 text += self.board[x][y].upper()
13 else:
14 text += self.board[x][y]
15 text += "\n"
16 return text
17
18 Marubatsu.__str__ = __str__
行番号のないプログラム
def __str__(self):
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn + "\n"
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status + "\n"
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
lastx, lasty = self.last_move
if x == lastx and y == lasty:
text += self.board[x][y].upper()
else:
text += self.board[x][y]
text += "\n"
return text
Marubatsu.__str__ = __str__
修正箇所
def __str__(self):
# ゲームの決着がついていない場合は、手番を表示する
if self.status == Marubatsu.PLAYING:
text = "Turn " + self.turn + "\n"
# 決着がついていれば勝者を表示する
else:
text = "winner " + self.status + "\n"
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
+ lastx, lasty = self.last_move
- if self.last_move is not None and x == self.last_move[0] and y == self.last_move[1]:
+ if x == lastx and y == lasty:
text += self.board[x][y].upper()
else:
text += self.board[x][y]
text += "\n"
return text
Marubatsu.__str__ = __str__
下記のプログラムを実行し、1,1
、0,0
、0,1
をテキストボックスに入力することで、直前の着手が大文字で表示されることが確認できます。実行結果は先程と同じなので省略します。
mb.play()
今回の記事のまとめ
今回の記事では、play
メソッドのさまざまな問題点を紹介し、それらの問題点を修正する方法について説明しました。今回の記事で紹介した以外の play
メソッドの改良案を思いついた方は、独自に改良することにチャレンジしてみて下さい。
〇×ゲームを遊べるようになったので、次回の記事からは、AI の実装を開始します。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
次回の記事