0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonで〇×ゲームのAIを一から作成する その22 手番の実装

Last updated at Posted at 2023-10-26

目次と前回の記事

実装の進捗状況と前回までのおさらい

〇×ゲームの仕様と進捗状況

  1. 正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う
  2. ゲーム開始時には、ゲーム盤の全てのマスは空になっている
  3. 2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ
  4. 2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く
  5. 先手は 〇 のプレイヤーである
  6. プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する
  7. すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする

仕様の進捗状況は、以下のように表記します。

  • 実装が完了した部分を 背景が灰色の長方形 で記述する
  • 実装の一部が完了した部分を、太字 で記述する

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

前回までのおさらい

前回までの記事で、クラスに関する説明を行いました。実装を進めるために必要な知識の説明を行ったため、長らく〇×ゲームの実装が滞っていましたが、重要な知識はかなり説明できたと思います。

手番の実装

下記の仕様 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 の代わりに、ローカル変数 textprint で表示していた 文字列を結合する という処理を行う点が異なります。下記の 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.turnmb の属性 なので、move メソッドの中で self.turn と記述することで利用することが出来ます。そのため、mb.turnmove メソッドの 仮引数 で受け取る 必要はありません。従って、 move メソッドの仮引数は、座標を表す xy2 つ になります。

mb.place_mark(1, 0, mb.turn)
if mb.turn == Marubatsu.CIRCLE:
    mb.turn = Marubatsu.CROSS
else:
    mb.turn = Marubatsu.CIRCLE

下記のプログラムが move メソッドの定義です。メソッドのブロックの中に記述するプログラムでは、上記のプログラムの mbself に置き替えて います。

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_markTrue を返した時だけ手番を変更 するように修正します。

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

式の中に記述する + などの記号のことを 演算子演算子以外の項目 の事を、 と呼びます。三項演算子の場合は、ifelse演算子 とみなされます。

三項演算子の名前の由来は、計算を行う際に、「式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 を 記述したほうが良い でしょう。

次回の記事

  1. リンク先の表の \n の行に表記されている「行送り」は、「改行」と同じ意味を表します

  2. - 演算子は、状況によって二項演算子であったり、単項演算子であったりします

  3. docstring は、関数やメソッドの呼び出しを 記述する際のヒント として記述します

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?