目次と前回の記事
実装の進捗状況と前回までのおさらい
〇×ゲームの仕様と進捗状況
正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
ゲーム開始時には、ゲーム盤の全てのマスは空になっている
2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
- 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
- 先手は 〇 のプレイヤーである
- プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
- すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする
仕様の進捗状況は、以下のように表記します。
- 実装が完了した部分を
背景が灰色の長方形
で記述する - 実装の一部が完了した部分を、太字 で記述する
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
前回までのおさらい
前回までの記事で、クラスに関する説明を行いました。実装を進めるために必要な知識の説明を行ったため、長らく〇×ゲームの実装が滞っていましたが、重要な知識はかなり説明できたと思います。
手番の実装
下記の仕様 4 から、〇×ゲームは、2 人のプレイヤー が遊ぶゲームであり、交互にマークを置く ことから、どのプレイヤー が次にマークを置くかを 表す、手番 があることがわかります。そこで、今回の記事では 手番に関する実装 を行うことにします。
4. 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
手番を表すデータ構造
過去の記事 で説明したように、データ をプログラムで 表現 するための 形式 の事を データ構造 と呼びます。手番を実装するためには、手番を表すデータ構造 を 決める 必要があります。
初心者の方は、手番のデータ構造を決めろと言われてもうまく思いつかない人が多いのではないかと思います。そのような場合は、手番 をプログラムではなく、人間の言葉で表現 する方法について考えてみると良いでしょう。下記は、筆者がぱっと思いついた手番の表現方法ですが、他にも様々な方法が考えられるでしょう。
- 仕様 3 から、プレイヤーは 〇 か × の マークを受け持つ ので、このマークを使って「〇 の手番」、「× の手番」のように表現する
- 最初にマークを配置するプレイヤーを「先手」、もう片方のプレイヤーを「後手」とし、「先手の手番」、「後手の手番」のように表現する
- プレイヤーの名前 を使って手番を表現する
上記の方法は、いずれも手番を 文字 を使って 表現 しています。従って、上記の表現方法を プログラムで行う 場合は、手番を 文字列 を使って 表現 することになります。上記のいずれの方法を採用しても〇×ゲームを実装することは可能ですが、これまでに実装してきた〇×ゲームでは、マスに配置した 〇 や × の マーク を、文字列型 の "o"
と "x"
で表現しました。同じ種類のデータ を 異なる方法 で表現すると、プログラムが わかりづらくなる ので、手番 を表すデータも 同じ方法 で表現することにします。
本記事では、手番を 文字列型のデータ で、ゲーム盤のマスに配置した マークと同じデータ構造(〇 は "o"
、× は "x"
)で表現することにします。
プログラムで手番を表現するデータ構造として良く使われる方法に、整数 を使って表現する方法があります。例えば、先手を 0
、後手を 1
で表現するという方法です。今回の記事ではこの方法は採用しませんが、この方法については今後の記事で実際に紹介します。
他にも、プレイヤーが 2 人の ゲームの場合は、先手 を論理型の True
、後手 を False
で表現するという方法などがあります。このように、手番を表すデータ構造は、扱いやすさの事を考慮しなければ いくらでも考える ことができます。
手番を管理する属性と手番の初期化
手番を表すデータ構造が決まったので、次は手番を表すデータを どこに記録するか について 決める 必要があります。これまでに実装したプログラムでは、ゲーム盤を表すデータ を Marubatsu
クラスの インスタンスの board
属性 に代入ました。そこで、手番を表すデータも同様に、Marubatsu
クラスの インスタンスの属性 に代入することにします。手番は英語で turn と表記するので、属性の名前 を turn
にすることにします。
次に、インスタンスの turn
属性の値を いつ、どのような値 に 設定する かについて考える必要があります。下記の仕様 5 から、〇×ゲームの 開始時 に 〇 の プレイヤーの 手番になる ことがわかるので、__init__
メソッドの中 でその処理を行うことにします。
5. 先手は 〇 のプレイヤーである
下記のプログラムは 1 行目で marubatsu
モジュールから Marubatsu
クラスをインポートし、8 行目で Marubatsu
クラスの __init__
メソッド を、3 ~ 6 行目で定義した関数で 上書きして修正 しています。
元の __init__
メソッド からの修正箇所は、6 行目で Marubatsu
クラスのインスタンスの turn
属性 に、〇 を表す Marubatsu.CIRCLE
を代入 している点です。
1 from marubatsu import Marubatsu
2
3 def __init__(self, board_size=3):
4 self.BOARD_SIZE = board_size
5 self.initialize_board()
6 self.turn = Marubatsu.CIRCLE
7
8 Marubatsu.__init__ = __init__
行番号のないプログラム
from marubatsu import Marubatsu
def __init__(self, board_size=3):
self.BOARD_SIZE = board_size
self.initialize_board()
self.turn = Marubatsu.CIRCLE
Marubatsu.__init__ = __init__
修正箇所
def __init__(self, board_size=3):
self.BOARD_SIZE = board_size
self.initialize_board()
+ self.turn = Marubatsu.CIRCLE
下記のプログラムは、1 行目で Marubatsu
クラスのインスタンスを作成し、2 行目でその turn
属性を表示しています。実行結果に、〇 を表す文字列である o
が表示されていることから、上記の修正が正しく実装されていることが確認できます。
mb = Marubatsu()
print(mb.turn)
実行結果
o
〇×ゲームの初期化を行うメソッドの定義
ゲーム盤の初期化と、手番の初期化は、いずれも 〇×ゲーム を 新しく開始 する際に 必要となる 処理です。このような処理は、Marubatsu
クラスからインスタンスを 作成した直後以外 でも、例えば〇×ゲームの 決着がついた後 で、新しく〇×ゲームを始める 場合など でも 必要となります。そこで、この 2 つの処理を記述した、〇×ゲームの初期化 を行う メソッドを定義 する事にします。
初期化は英語で initialize
ですが、これをそのままメソッドの名前にすると、__init__
と 名前が似ていて 若干 紛らわしい 気がするので本記事では 再起動 を表す restart
という名前のメソッドにすることにします。
他の名前の候補としては、reset
などが挙げられるでしょう。変数、関数、メソッドなどの名前をどのように付けるかは、人によって個性が出る 部分だと思います。また、名前の付け方に 唯一の正解はありません。このメソッドの名前に限らず、他の名前の方が良いと思った方は、変更するのが面倒でなければ自由に変更してもらってもかまいません。ただし、プログラムがわかりにくくなるので、名前は 一貫した法則 で付けることを強くお勧めします。
restart
メソッドで行う処理は、現時点では ゲーム盤の初期化 と 手番の初期化 なので、下記のプログラムのように定義することが出来ます。
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
Marubatsu.restart = restart
また、__init__
メソッドを restart
を使って下記のように定義し直すことにします。
〇×ゲームの初期化を行う際に、ゲーム盤のサイズを変更する必要はないので、self.BOARD_SIZE = board_size
の処理は、インスタンスを作成する際に __init__
メソッドの中 一度だけ実行 すれば十分です。
def __init__(self, board_size=3):
self.BOARD_SIZE = board_size
self.restart()
Marubatsu.__init__ = __init__
修正箇所
def __init__(self, board_size=3):
self.BOARD_SIZE = board_size
- self.initialize_board()
- self.turn = Marubatsu.CIRCLE
+ self.restart()
下記は、Marubatsu クラスが正しく動作するかどうかを確認するプログラムです。実行結果から、〇×ゲームの開始時に 〇 の手番になっていることが確認できます。
mb = Marubatsu()
print(mb.turn)
実行結果
o
手番の表示
ここまでの修正で、手番を表す属性を追加しましたが、display_board
は 修正していません。そのため、ゲーム盤を表示した際に、下記のプログラムの実行結果のように、ゲーム盤の マスの情報しか表示されない ので、どちらの手番であるかがわからない という問題があります。
mb.display_board()
実行結果
...
...
...
そこで、display_board
を、下記のプログラムのように ゲーム盤の上 にどちらの 手番 であるかを 表示 するように修正することにします。下記のプログラムでは、ゲーム盤を表示する前 の 2 行目で、手番を表す文字列 を 表示 しています。
1 def display_board(self):
2 print("Turn", self.turn)
3 for y in range(self.BOARD_SIZE):
4 for x in range(self.BOARD_SIZE):
5 print(self.board[x][y], end="")
6 print()
7
8 Marubatsu.display_board = display_board
行番号のないプログラム
def display_board(self):
print("Turn", self.turn)
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
print(self.board[x][y], end="")
print()
Marubatsu.display_board = display_board
修正箇所
def display_board(self):
+ print("Turn", self.turn)
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
print(self.board[x][y], end="")
print()
上記の修正を行うことで、下記のプログラムの実行結果のように、display_board
メソッドでゲーム盤の上に手番を表す情報が表示されるようになります。
mb.display_board()
実行結果
Turn o
...
...
...
手番を表す文字列をどこにどのように表示するかは、自由に変更してもらってもかまいません。例えばゲーム盤の下に表示する場合は、上記の 2 行目のプログラムを 6 行目の後に移動します。
メソッドの修正と名前の変更
上記の修正によって、display_board
メソッドが、手番とゲーム盤の両方を表示するようになりました。display_board
という名前は、ゲーム盤(board)を表示(display)するという意味なので、手番を表示するように修正したことで、メソッドの名前 とメソッドが行う 処理の内容 が 一致しなくなります。
このような場合は、メソッドの 名前を 例えば display_game
のように 修正する という方法が考えられます。また、そのような細かい違いは 重要ではないと考えて、メソッドの 名前を変更しない という方法も考えられるでしょう。どちらを選ぶかは自由ですが、メソッドの名前と処理の内容が 大きくかけ離れてしまう ような場合は、メソッドの 名前を修正したほうが良い でしょう。
なお、display_board
メソッドに関しては、次で行う別の修正によって、このメソッドそのものが必要となくなり 削除することになる ので、メソッドの名前は変更しません。
__str__
メソッド
ところで、〇×ゲームを表示 する際に、mb.display_board()
のように、メソッドの呼び出しを記述 するのは 面倒 だと思いませんか?また、この方法で〇×ゲームを表示するためには、〇×ゲームを表示するためのメソッドが display_board
であるということを 覚えておく必要 があります。
それに対して、数値型、文字列型、list などの、Python の 組み込みデータ型を表示 する場合は、組み込み関数 print
を使うことで、何らかのメソッドを呼び出すことなく、共通の記述方法 でその内容を表示することが出来ます。
Marubatsu
クラスのインスタンスも同様に、print(mb)
のように記述することで〇×ゲームが表示されるようになると 便利 です。しかし、実際には print(mb)
を実行すると下記の実行結果のような表示が行われてしまいます。
print(mb)
実行結果
<marubatsu.Marubatsu object at 0x0000027E7D436550>
Python では、__str__
という名前の 特殊メソッド を クラスに定義 する事で、そのクラスから作成された インスタンスを表す文字列 を 自分で設定 し、組み込み関数 print
を使って print(mb)
のように記述して表示することが できるように なります。
特殊メソッド __str__
は、仮引数 に self
だけ を持つメソッドで、文字列型 のデータを 返り値として返す ように 定義 します。__str__
メソッドが定義されたクラスから作成されたインスタンスを、実引数として組み込み関数 print
を呼び出す と、インスタンスから __str__
メソッドが呼び出され、その返り値が表示 されるようになります。
具体例を挙げて説明します。下記のプログラムでは、Marubatsu
クラスに、常に "〇×ゲーム"
という 文字列 を返す __str__
メソッドを定義しています。
def __str__(self):
return "〇×ゲーム"
Marubatsu.__str__ = __str__
上記の修正を行った結果、下記のプログラムの実行結果のように、Marubatsu
クラス から作成された インスタンス を print
の実引数に記述して表示すると、常に __str__
メソッドの返り値である "〇×ゲーム"
が表示 されるようになります。
print(mb)
実行結果
〇×ゲーム
Marubatsu
クラスの __str__
メソッドの定義
下記のプログラムは、print(mb)
によって mb.display_board()
と同じ表示 が行われるように、Marubatsu
クラスの __str__
メソッドを修正したプログラムです。
__str__
メソッドは、〇×ゲームを表す 文字列を返す 必要があるので、text
という名前の ローカル変数 にその文字列を代入することにします。2 ~ 6 行目で行われる処理の具体的な内容についてはこの後で説明しますが、下記のプログラムでは、2 ~ 6 行目で ローカル変数 text
に 〇×ゲームを表す文字列 を計算して 代入 し、7 行目で text
を 返り値として返しています。
1 def __str__(self):
2 text = "Turn " + self.turn + "\n"
3 for y in range(self.BOARD_SIZE):
4 for x in range(self.BOARD_SIZE):
5 text += self.board[x][y]
6 text += "\n"
7 return text
8
9 Marubatsu.__str__ = __str__
行番号のないプログラム
def __str__(self):
text = "Turn " + self.turn + "\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__
上記のプログラムの 2 行目では、手番を表す文字列 を text
に代入 しています。文字列を表すデータは、+
演算子 を使うことで、結合 することが出来ます。また、2 行目の最後に記述している "\n"
は、この 2 文字 で 1 つの改行を表す 文字列です。\
は日本語のキーボードでは ¥
が印字されたキー を押すことで入力できる、バックスラッシュ と呼ばれる 半角の文字 です。
バックスラッシュ の 直後 に 特定の文字を記述 するという、エスケープシーケンス という方法で、"
や '
で囲われた 文字列のリテラルの中 に、改行などの 特殊な意味を持つ文字 を記述することが出来ます。良く使われるエスケープシーケンスには、改行を表す \n
があります。エスケープシーケンスの一覧については、下記のリンク先を少し下にスクロールした所に記述されている表1を参照して下さい。
5、6 行目に記述されている +=
は、前回の記事で使いましたが、前回の記事では簡単な説明しかしなかったので、もう少し詳しく説明します。
+=
を使った代入文は、累算代入文 と呼ばれ、「左に記述した変数の値」と「右に記述した式」を +
演算子 で演算し、その 計算結果 を左に記述した 変数に代入 するという処理を行います。-=
、*=
、/=
などを使って、同様の処理を行う累算代入文を記述することが出来ます。累算代入文の詳細については、下記のリンク先を参照して下さい。
例えば、下記の 1 行目と 2 行目のプログラムは、同じ処理を行うプログラムです。累算代入文は、変数の値を加算するような処理を 簡潔に記述 できるので 良く使われます。
test += self.board[x][y]
test = test + self.board[x][y]
+=
の左に記述した変数に代入されている データ型の種類 によっては、上記の 2 つの式が 異なる処理を行う 場合があります。例えば、変数に list が代入されている場合は、+=
は list の 拡張、+
演算子は list の 結合 という異なる処理を行います。list の結合と拡張に関する詳細は 過去の記事 を参照して下さい。
数値型、文字列型、tuple のデータに対しては、上記の 2 つの式は 同一の処理 が行われますが、それ以外 のデータ型に関しては 異なる処理 が行われる 可能性がある ので、+=
を利用する際には 注意が必要 です。
__str__
メソッドに記述した、2 重の for 文のブロックの中に記述したプログラムは、display_board
メソッドのプログラムに似ていますが、print
の代わりに、ローカル変数 text
に print
で表示していた 文字列を結合する という処理を行う点が異なります。下記の display_board
からの修正箇所と見比べて下さい。
display_board
からの修正箇所
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
- print(self.board[x][y], end="")
+ text += self.board[x][y]
- print()
+ text += "\n"
return text
上記の修正によって、下記のプログラムの実行結果のように、print(mb)
を記述することで、〇×ゲームを表示することが出来るようになります。また、この修正によって、display_board
メソッド は 必要がなくなった ので 削除する ことにします。
print(mb)
実行結果
Turn o
...
...
...
__str__
に似た 文字列を返す 特殊メソッドに __repr__
メソッドがあります。この 2 つのメソッドは以下のような違いがあります。
-
__str__
メソッドは、非公式の、あるいは 表示に適した文字列 表現(人間にとってわかりやすい文字列)を返すように定義する。主にprint
の実引数 に記述した際などで利用される -
__repr__
メソッドは、オブジェクトを表す公式の文字列を返すように定義する。print
の実引数に記述した際には基本的には 利用されない
詳細は、下記のリンク先を参照して下さい。
__repr__
メソッドの説明の、「オブジェクトを表す公式の文字列」が何であるかについての説明は長くなるので省略しますが、__str__
と __repr__
は 異なる目的 で定義する特殊メソッドであることは覚えておいてください。なお、__str__
が 表示に適した文字列表現 を返すと説明してあるように、〇×ゲームのゲーム盤を print
で表示 する場合は、__str__
メソッドを定義すべき です。また、本記事では必要がないので __repr__
メソッドは定義しません。
着手の実装
手番を表す属性と、〇×ゲームの表示が実装できたので、次は 自分の手番 でゲーム盤に マークを配置する という処理を実装します。一般的に、ゲームで自分の手番で何らかの操作を行うことを 着手 と呼びます。
これまでは、ゲーム盤のマスに マークを配置する処理 を mb.place_mark(0, 0, Marubatsu.CIRCLE)
のように記述してきましたが、配置する マーク は 手番 を表す、turn
属性に代入 されているので、下記のプログラムのように記述すことが出来ます。
下記のプログラムは、(0, 0) のマスに手番のマークを配置しており、実行結果から (0, 0) のマスに先手である 〇 のマークが配置されていることが確認できます。
実は、下記の実行結果には おかしな点が 1 つ あります。それが何であるかについて 考えてみて下さい。
mb.place_mark(0, 0, mb.turn)
print(mb)
実行結果
Turn o
o..
...
...
おかしな点は、〇 のマークを配置後に、手番が 〇のまま である点です。これは、マークの配置後に必要となる、手番を入れ替える処理 が 記述されていない ことが原因です。
手番を入れ替える処理は、手番 が 〇 の場合は × に、× の場合は 〇 にする処理なので、if 文 を使って下記のプログラムの 1 ~ 4 行目のように記述することが出来ます。
6 行目の実行結果から、手番が × に変わったことを確認することが出来ます。
1 if mb.turn == Marubatsu.CIRCLE:
2 mb.turn = Marubatsu.CROSS
3 else:
4 mb.turn = Marubatsu.CIRCLE
5
6 print(mb)
行番号のないプログラム
if mb.turn == Marubatsu.CIRCLE:
mb.turn = Marubatsu.CROSS
else:
mb.turn = Marubatsu.CIRCLE
print(mb)
実行結果
Turn x
o..
...
...
上記のプログラムの後で、続けて × の手番で (1, 0) のマスにマークを配置する処理は、先程と同様に、下記のプログラムの 1 ~ 5 行目のように記述することが出来ます。実行結果から × の手番でもマークを配置後に手番が正しく変化することが確認できます。
1 mb.place_mark(1, 0, mb.turn)
2 if mb.turn == Marubatsu.CIRCLE:
3 mb.turn = Marubatsu.CROSS
4 else:
5 mb.turn = Marubatsu.CIRCLE
6
7 print(mb)
行番号のないプログラム
mb.place_mark(1, 0, mb.turn)
if mb.turn == Marubatsu.CIRCLE:
mb.turn = Marubatsu.CROSS
else:
mb.turn = Marubatsu.CIRCLE
print(mb)
実行結果
Turn o
ox.
...
...
着手を行うメソッドの定義
着手を行う処理を記述することが出来るようなりましたが、着手を 行うたび に毎回上記のような 5 行分のプログラムを記述することは 大変 なので、着手の処理を行う メソッドを定義 する事にします。そのためには メソッドの名前を決める 必要があります。
色々と調べてみた所、将棋やチェスのように、ゲーム盤上でコマを動かすような場合は、英語で着手を表す用語は move と表現されるようです。また、囲碁やオセロのように、ゲーム盤上にコマを(動かさずに)配置するような場合でも、下記のリンク先のように着手は place ではなく、move という用語が使われているようなので、本記事でも着手を行うメソッドの名前を move
とすることにします。
下記は、先ほどの着手を行うプログラムを再掲したものです。下記で 着手を行う際 に 利用するデータ は、マークを配置する 座標 と、mb.turn
ですが、mb.turn
は mb
の属性 なので、move
メソッドの中で self.turn
と記述することで利用することが出来ます。そのため、mb.turn
を move
メソッドの 仮引数 で受け取る 必要はありません。従って、 move
メソッドの仮引数は、座標を表す x
と y
の 2 つ になります。
mb.place_mark(1, 0, mb.turn)
if mb.turn == Marubatsu.CIRCLE:
mb.turn = Marubatsu.CROSS
else:
mb.turn = Marubatsu.CIRCLE
下記のプログラムが move
メソッドの定義です。メソッドのブロックの中に記述するプログラムでは、上記のプログラムの mb
を self
に置き替えて います。
def move(self, x, y):
self.place_mark(x, y, self.turn)
if self.turn == Marubatsu.CIRCLE:
self.turn = Marubatsu.CROSS
else:
self.turn = Marubatsu.CIRCLE
Marubatsu.move = move
下記のプログラムは、1 行目で restart
メソッドを呼び出して〇×ゲームを再起動し、move
メソッドを使って 2 行目で 〇 の手番で (0, 0) にマークを配置し、4 行目で × の手番で (1, 0) にマークを配置するプログラムです。実行結果から、正しいマスにマークが配置され、手番も正しく変化することが確認できます。
mb.restart()
mb.move(0, 0)
print(mb)
mb.move(1, 0)
print(mb)
実行結果
Turn x
o..
...
...
Turn o
ox.
...
...
これで、move
メソッドが正しく動作することが確認できたように 見えるかもしれません が、この move
メソッドには 重大なバグ があります。そのバグが何であるかについて 考えてみて下さい。
move
メソッドのバグ
move
メソッドのバグは、既にマークが配置されているマス にマークを 配置しようとした場合 でも、手番が変わってしまう というものです。下記は、先ほどのプログラムに続けて、〇 の手番で もう一度 (0, 0) のマスにマークを配置するプログラムです。
実行結果で表示されるメッセージから、(0, 0) のマスにマークが配置済であることがわかり、実際にその下の表示から (0, 0) のマークは 〇 のままであることを確認することが出来ます。しかし、マークを配置できなかったにも関わらず、Turn x
の表示から、手番が × に変わっている ことがわかります。
mb.move(0, 0)
print(mb)
実行結果
( 0 , 0 ) のマスにはマークが配置済です
Turn x
ox.
...
...
このバグは、move
メソッドの中で、place_mark
を 呼び出した後 で、必ず手番を入れ替える 処理を行っていることが原因です。従って、このバグを修正するためには、place_mark
で マークを配置したときだけ、手番を入れ替えるようにプログラムを修正する必要があります。ただし、現時点での place_mark
は 値を返さない関数 なので、このままでは、place_mark
を呼び出した結果、マークが配置されたかどうかを 知ることはできません。
そこで、下記のプログラムのように、place_mark
を、マークを 配置できた場合 は True を、配置できなかった場合 は False を 返り値として返す ように修正することにします。
修正した部分は、マークを配置した後 の 4 行目に return 文で True
を返す 処理を追加した部分と、マークを配置できなかった場合 に 7 行目に return 文で False
を返す 処理を追加した部分です。
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
5 else:
6 print("(", x, ",", y, ") のマスにはマークが配置済です")
7 return False
8
9 Marubatsu.place_mark = place_mark
行番号のないプログラム
def place_mark(self, x, y, mark):
if 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 self.board[x][y] == Marubatsu.EMPTY:
self.board[x][y] = mark
+ return True
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
+ return False
次に、move
メソッドを下記のプログラムのように、2 行目に if 文を追加 することで、place_mark
が True
を返した時だけ手番を変更 するように修正します。
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
3 if self.turn == Marubatsu.CIRCLE:
4 self.turn = Marubatsu.CROSS
5 else:
6 self.turn = Marubatsu.CIRCLE
7
8 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
if self.turn == Marubatsu.CIRCLE:
self.turn = Marubatsu.CROSS
else:
self.turn = Marubatsu.CIRCLE
Marubatsu.move = move
修正箇所
def move(self, x, y):
+ if self.place_mark(x, y, self.turn):
- if self.turn == Marubatsu.CIRCLE:
- self.turn = Marubatsu.CROSS
- else:
- self.turn = Marubatsu.CIRCLE
+ if self.turn == Marubatsu.CIRCLE:
+ self.turn = Marubatsu.CROSS
+ else:
+ self.turn = Marubatsu.CIRCLE
下記のプログラムは、1 行目で restart
メソッドを呼び出して〇×ゲームを再起動した後で、2 ~ 4 行目で move
メソッドを呼び出して (0, 0) のマスに 3 回マークを着手 する処理を行っています。実行結果から、最初の着手 では (0, 0) のマスに 〇 を配置 し、手番が変わっています が、それ以降の着手 では (0, 0) のマスのマークは変化せず、手番も変わっていないことが確認できます。
mb.restart()
for i in range(3):
mb.move(0, 0)
print(mb)
実行結果
Turn x
o..
...
...
( 0 , 0 ) のマスにはマークが配置済です
Turn x
o..
...
...
( 0 , 0 ) のマスにはマークが配置済です
Turn x
o..
...
...
三項演算子
move
メソッドでは、手番の交代を行う処理を、下記のプログラムで記述しました。
if self.turn == Marubatsu.CIRCLE:
self.turn = Marubatsu.CROSS
else:
self.turn = Marubatsu.CIRCLE
このような、特定の条件 によって、特定の変数 に 異なる値を代入 する処理は、三項演算子 を使って 1 行のプログラム で簡潔に 記述する ことが出来ます。
三項演算子は下記のように記述し、実行すると、条件式が True の場合は 式1 の計算結果、条件式が False の場合は 式2 の計算結果 になります。
式1 if 条件式 else 式2
三項演算子を利用することで、先ほどの手番の交代を行う処理は、下記のプログラムのように、1 行で記述 することが出来ます。
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
式の中に記述する +
などの記号のことを 演算子、演算子以外の項目 の事を、項 と呼びます。三項演算子の場合は、if
と else
も 演算子 とみなされます。
三項演算子の名前の由来は、計算を行う際に、「式1」、「条件式」、「式2」の 3 つの項 を使って演算を行うことから来ています。+
や -
のような 前後の 2 つの項 を使って演算を行う演算子は 二項演算子、後ろの 1 つの項 の符号を反転する -
演算子2のことを 単項演算子 と呼びます。
下記が、三項演算子を使うように修正した __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
修正箇所
def move(self, x, y):
if self.place_mark(x, y, self.turn):
- if self.turn == Marubatsu.CIRCLE:
- self.turn = Marubatsu.CROSS
- else:
- self.turn = Marubatsu.CIRCLE
+ self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
三項演算子 を使うと、上記の例のように、プログラムを 短く記述する ことが出来ますが、if 文を使った場合と比べてプログラムの 見た目がわかりづらくなる という欠点があります。本記事で上記のプログラムより見た目がわかりづらくなるよう場合は、三項演算子を使わないことにしますが、実際に三項演算子が 使われることはよくある ので、その 意味と使い方は覚えておいたほうが良い でしょう。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu_new.py です。なお、__str__
などの 特殊メソッド は、基本的に直接 呼び出して実行することはない3 ので docstring は一般的に 記述しません。ただし、__init__
メソッドは例外で、インスタンスを作成する際に 実引数を記述 して呼び出す場合は docstring を 記述したほうが良い でしょう。
次回の記事