目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
前回までのおさらい
前回の記事で、〇×ゲームを遊ぶための play
メソッドの実装が完了しました。
今回の記事からいよいよ 〇×ゲームの AI の作成を開始します。
〇×ゲームの AI の処理を行う関数
〇×ゲームの AI が行う処理 は、〇×ゲームの 局面 から、次に 着手 を行う マスの座標を計算 するというものです。これから、さまざまな〇×ゲームのAI を作成することになるので、作成する AI ごと に、下記のような処理を行う 関数を定義 することにします。
処理:〇×ゲームの 局面 から、次に 着手 を行う マスの座標 を 計算 する
名前:それぞれの AI ごと に ai1
、ai2
のような 名前をつける
入力:mb
という名前の 仮引数 に Marubatsu
クラスの インスタンスを代入 する
出力:返り値 として、次に 着手 を行う マスの座標 を表す (x, y)
という tuple を 返す
さまざまな AI を作成するので、それぞれの AI の関数の名前を凝った名前にすると、名前を考える のが 大変 になります。そこで、ai1
のような 番号を使った名前 にします。
具体的には下記のような関数を定義します。
def ai1(mb):
mb から着手を行う座標 (x, y) を計算する処理
return x, y
本記事では、〇×ゲームの AI の関数 は、Marubatsu
クラスの メソッドではなく、関数として定義 することにします。その理由の一つは、Marubatsu
クラスのメソッドとして AI の処理を記述すると、AI の数だけ メソッドを 定義する必要 が生じるため、Marubatsu
クラスの 定義 が 非常に長く なってしまい、プログラムが わかりづらく なるためです。他の理由としては、〇×ゲームを 管理して遊ぶための処理 と、AI の処理 は 別の処理 なので、別々に記述したほうがよいだろうと判断したためです。なお、Marubatsu
クラスの メソッドとして AI を 定義 することは実際に 可能 で、そのような記述が 間違っているわけではありません ので、そちらの方が良いと判断した場合は、そのように記述してもかまいません。
最初に見つかった空いているマスに着手を行う AI
最初に作成する AI として、最も簡単 に 作成 できる、下記のような アルゴリズム で着手を行う ai1
という名前の関数を定義することにします。
- 左上のマスから順番に 空いているマス を 探す
- 最初に見つかった 空いている マス を着手を行うマスとして 選択 する
上記の手順 1 では、左上のマスから順番にとしていますが、空いているマスを 探す順番 を、例えば右下から順番に探すように 変更 しても 構いません。
上記のようなアルゴリズムで着手を行う AI は、全然 AI のように見えないかもしれませんが、これでも れっきとした AI です。もちろん、このアルゴリズムで作成された AI は強い AI には決してなりませんが、AI と人間の対戦 や、AI どうしの対戦 を行うことができます。
ai1
の定義
下記は上記のアルゴリズムで着手を選択するプログラムです。
-
2、3 行目:2 重の for 文 によって、
mb
の すべてのマス に対する 繰り返し処理 を行う - 4、5 行目:(x, y) のマスが 空であるか どうかを 判定 し、空の場合 は (x, y) を返す
from marubatsu import Marubatsu
def ai1(mb):
for y in range(mb.BOARD_SIZE):
for x in range(mb.BOARD_SIZE):
if mb.board[x][y] == Marubatsu.EMPTY:
return x, y
上記の ai1
は、空いているマス が一つも 存在しない場合 は、5 行目 の return 文 が 実行されることは無い ので、2 重の for 文 が 必ず終了 し、return 文 が 実行されず に 関数 のブロックの 処理が終了 するので、None
が返り値として 返ります。
ただし、〇×ゲーム では、空いている マスが一つも 存在しない場合 は、必ず ゲームの 決着がついている ので、その場合に ai1
が 呼ばれること は あり得ない ので、その点について 気にする必要はない でしょう。
下記は、ゲームの 開始直後 と、(0, 0) に着手を行った後 で ai1
を呼び出して 次の着手を表示 するプログラムです。実行結果から、ゲームの 開始直後 の 4 行目の場合は、左上 の (0, 0) のマスが 空いている ので (0, 0) が、(0, 0) に 着手を行った後 の 6 行目の場合は、その隣 の (1, 0) が 表示される ことが確認できます。
mb = Marubatsu()
print(ai1(mb))
mb.move(0, 0)
print(ai1(mb))
実行結果
(0, 0)
(1, 0)
ai1
どうしの対戦
下記は ai1
どうし で 対戦 を行うプログラムです。このプログラムは、play
メソッドの ブロックの中 で行っている処理を 抜き出して、input
を使って キーボードから着手を行う座標を入力 する 部分 を、ai1
を呼び出して AI が着手を選択する ように 修正 しています。
なお、ai1
は、必ず ゲーム盤の マスを表す座標 を 返す ので、play
メソッドにあった、"exit"
に関する処理など の、行う必要がない処理 は 削除 しました。実行結果から、左上のマスから順番 に 着手 が行われ、7 手目で 〇 が勝利することが確認できます。
mb = Marubatsu()
while mb.status == Marubatsu.PLAYING:
print(mb)
x, y = ai1(mb)
mb.move(x, y)
print(mb)
実行結果
Turn o
...
...
...
Turn x
O..
...
...
Turn o
oX.
...
...
Turn x
oxO
...
...
Turn o
oxo
X..
...
Turn x
oxo
xO.
...
Turn o
oxo
xoX
...
winner o
oxo
xox
O..
play
メソッドの改良
ai1
と人間 の 対戦 も、play
メソッドの 処理を抜き出して修正 することで行うことができますが、毎回そのようなプログラムを記述するのは 大変 です。また、行う処理 は play
メソッドで行う処理と 大きく変わらない ので、play
メソッドを、「人間どうし」、「人間と AI」、「AI どうし」の 対戦 を 行うことができる ように 改良 することにします。どのように修正すればよいかについて少し考えてみて下さい。
〇 の担当の選択の実装
まず、〇 を 人間が担当 するか、ai1
が担当 するかを 選択できるよう に 改良 することにします。そのためには、play
メソッドに、そのことを 選択できるようにする ための 仮引数を追加 する必要があります。そこで、ai_circle
という名前の 仮引数 に、True
が代入 された場合は ai1
が、False
が代入 された場合は 人間 が 〇 を担当 することにします。下記は、そのように play
メソッドを修正したプログラムです。
-
9、10 行:〇 の手番 で、なおかつ
ai_circle
がTrue
の場合に 10 行目でai1
を呼び出し、その 返り値 を展開して 着手 を行う 座標 をx
とy
に代入 する -
11 ~ 26 行目:9 行目の if 文の 条件式が
False
の場合は、元の処理 を行う
1 def play(self, ai_circle):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲームの決着がついていない間繰り返す
5 while self.status == Marubatsu.PLAYING:
6 # ゲーム盤の表示
7 print(self)
8 # 〇の手番で、ai_circle が True の場合は ai が着手を行う
9 if self.turn == Marubatsu.CIRCLE and ai_circle:
10 x, y = ai1(self)
11 else:
12 # キーボードからの座標の入力
13 coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
14 # "exit" が入力されていればメッセージを表示して関数を終了する
15 if coord == "exit":
16 print("ゲームを終了します")
17 return
18 # x 座標と y 座標を要素として持つ list を計算する
19 xylist = coord.split(",")
20 # xylist の要素の数が 2 ではない場合
21 if len(xylist) != 2:
22 # エラーメッセージを表示する
23 print("x, y の形式ではありません")
24 # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
25 continue
26 x, y = xylist
27 # (x, y) に着手を行う
28 try:
29 self.move(int(x), int(y))
30 except:
31 print("整数の座標を入力して下さい")
32
33 # 決着がついたので、ゲーム盤を表示する
34 print(self)
35
36 Marubatsu.play = play
行番号のないプログラム
def play(self, ai_circle):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# 〇の手番で、ai_circle が True の場合は ai が着手を行う
if self.turn == Marubatsu.CIRCLE and ai_circle:
x, y = ai1(self)
else:
# キーボードからの座標の入力
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
修正箇所
分かりづらくなるので、else のブロックのインデント は 修正箇所 に 含めません。
-def play(self):
+def play(self, ai_circle):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# 〇の手番で、ai_circle が True の場合は ai が着手を行う
+ if self.turn == Marubatsu.CIRCLE and ai_circle:
+ x, y = ai1(self)
+ else:
# キーボードからの座標の入力
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
下記のプログラムを実行すると、〇 を ai1
が、× を人間 が 担当 して 〇×ゲームを遊ぶことができます。下記の実行結果は 1,1、2,2 の順でテキストボックスに入力した場合で、ai1
は、アルゴリズムの通り に (0, 0)、(1, 0)、(2, 0) の順で着手を行い、〇 の勝利となります。
mb.play(True)
実行結果
Turn o
...
...
...
Turn x
O..
...
...
Turn o
o..
.X.
...
Turn x
oO.
.x.
...
Turn o
oo.
.x.
..X
winner o
ooO
.x.
..x
新しい機能を実装 した 場合 は、新しい機能を 実装した結果、従来の機能 が 正しく動作しなくなる可能性 があります。そのため、新しい機能 が 正しく動作するか どうかの 確認だけでなく、従来の機能 が 正しく動作するかどうか の 確認 を行う 必要 があります。
下記のプログラムを実行すると、これまでと同様に、人間どうし で〇×ゲームを遊ぶことができます。下記の実行結果は 0,0、0,1、1,0、1,1、2,0 の順でテキストボックスに入力した場合で、人間どうしの対戦を問題なく行えることが確認できます。
mb.play(False)
実行結果
Turn o
...
...
...
Turn x
O..
...
...
Turn o
o..
X..
...
Turn x
oO.
x..
...
Turn o
oo.
xX.
...
winner o
ooO
xx.
...
× の担当の選択の実装
次に、× を 人間が担当 するか、ai1
が担当 するかを 選択できる ように 修正 します。先ほどと同様 に、play
メソッドに、そのことを選択できるようにするための ai_cross
という名前の 仮引数を追加 し、処理を記述 ます。下記は、そのように修正したプログラムです。
-
9、10 行目:元の 条件式の条件または、× の手番 で、なおかつ
ai_cross
がTrue
の場合を判定するように条件式を修正する
なお、play
メソッドがかなり長くなったので、以後は 長い 関数やメソッドの 定義 と 修正箇所 は、修正 した部分の 付近 のプログラム のみを表記 することにします。ただし、行番号のないプログラム のほうには すべてのプログラムを記述 します。
1 def play(self, ai_circle, ai_cross):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲームの決着がついていない間繰り返す
5 while self.status == Marubatsu.PLAYING:
6 # ゲーム盤の表示
7 print(self)
8 # ai が着手を行うかどうかを判定する
9 if (self.turn == Marubatsu.CIRCLE and ai_circle) or \
10 (self.turn == Marubatsu.CROSS and ai_cross):
11 x, y = ai1(self)
12 else:
13 # キーボードからの座標の入力
以下同じなので省略
行番号のないプログラム
def play(self, ai_circle, ai_cross):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# ai が着手を行うかどうかを判定する
if (self.turn == Marubatsu.CIRCLE and ai_circle) or \
(self.turn == Marubatsu.CROSS and ai_cross):
x, y = ai1(self)
else:
# キーボードからの座標の入力
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, ai_circle):
+def play(self, ai_circle, ai_cross):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# ai が着手を行うかどうかを判定する
- if self.turn == Marubatsu.CIRCLE and ai_circle:
+ if (self.turn == Marubatsu.CIRCLE and ai_circle) or \
+ (self.turn == Marubatsu.CROSS and ai_cross):
x, y = ai1(self)
else:
# キーボードからの座標の入力
以下同じなので省略
以前の記事 で説明したように、and 演算子 の方が、or 演算子 より 優先順位が高い ので、上記のプログラムの 9、10 行目の条件式は、下記のプログラムのように ()
を省略 することが できます。ただし、下記のプログラムのように記述すると、条件式で行う計算 が わかりづらくなる ので、上記のプログラムでは記述する 必要のない ()
を 記述 しています。必要がないと思った方は ()
を削除しても構いません。
if self.turn == Marubatsu.CIRCLE and ai_circle or \
self.turn == Marubatsu.CROSS and ai_cross:
下記のプログラムを実行すると、× を ai1
が、〇 を人間 が 担当 して 〇×ゲームを遊ぶことができます。下記の実行結果は 0,0、1,1、2,2 の順でテキストボックスに入力した場合で、ai1
は、アルゴリズムの通り に (1, 0)、(2, 0) の順で着手を行い、〇 の勝利となります。
mb.play(False, True)
実行結果
Turn o
...
...
...
Turn x
O..
...
...
Turn o
oX.
...
...
Turn x
ox.
.O.
...
Turn o
oxX
.o.
...
winner o
oxx
.o.
..O
下記は、残り の「両方とも人間が担当」、「〇 を ai1
が担当」、「両方とも ai1
が担当」する場合の 確認 するプログラムです。長くなるので実行結果は省略します(github のほうでは実際に実行しています)が、正しく実行されることが確認できます。
mb.play(False, False)
mb.play(True, False)
mb.play(True, True)
仮引数の改良
上記のプログラムでは、2 つ の 仮引数 ai_circle
と ai_cross
によって それぞれのマーク を人間か ai1
の どちらが担当 するかを 判定 していましたが、これを 1 つの仮引数 に まとめる ことができます。その場合は、list を使って 〇 と × を どちらが担当 するかを 指定 し、list の 0 番の要素 が 〇 を、1 番の要素 が × を ai1
が 担当するかどうか を表します。
下記は、そのように play
メソッドを修正したプログラムです。
-
1 行目:仮引数 を
is_ai
のみ に修正する -
9、10 行目:
is_ai[0]
が 〇 を、ai[1]
が × を、ai1
が 担当するかどうか を 表す ので、ai_circle
をis_ai[0]
に、ai_cross
をis_ai[1]
に 修正 する
1 def play(self, is_ai):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲームの決着がついていない間繰り返す
5 while self.status == Marubatsu.PLAYING:
6 # ゲーム盤の表示
7 print(self)
8 # ai が着手を行うかどうかを判定する
9 if (self.turn == Marubatsu.CIRCLE and is_ai[0]) or \
10 (self.turn == Marubatsu.CROSS and is_ai[1]):
11 x, y = ai1(self)
12 else:
13 # キーボードからの座標の入力
以下同じなので省略
行番号のないプログラム
def play(self, is_ai):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# ai が着手を行うかどうかを判定する
if (self.turn == Marubatsu.CIRCLE and is_ai[0]) or \
(self.turn == Marubatsu.CROSS and is_ai[1]):
x, y = ai1(self)
else:
# キーボードからの座標の入力
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, ai_circle, ai_cross):
+def play(self, is_ai):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# ai が着手を行うかどうかを判定する
- if (self.turn == Marubatsu.CIRCLE and ai_circle) or \
+ if (self.turn == Marubatsu.CIRCLE and is_ai[0]) or \
- (self.turn == Marubatsu.CROSS and ai_cross):
+ (self.turn == Marubatsu.CROSS and is_ai[1]):
x, y = ai1(self)
else:
# キーボードからの座標の入力
以下同じなので省略
実行結果は省略しますが、下記のプログラムで、人間と ai1
の 4 通りの組み合わせ で対戦を行うことができることを確認できます。
mb.play([False, False])
mb.play([True, False])
mb.play([False, True])
mb.play([True, True])
修正前 で記述していた、mb.play(False, False)
と 比較 して、[
と ]
を 記述する必要 があるので、この修正は 改良になっていない と 思う人 がいる かもしれない ので説明します。
この 修正 は、キーワード引数 を使った場合に 有効 となる 修正 です。以前の記事で説明したように、関数呼び出し の際に 複数の実引数 を 記述 した場合、それぞれの 実引数の意味 が 分かりづらく なりがちですが、キーワード引数 を使って 実引数を記述 することで、その 問題を緩和 することができます。例えば、下記の 1 行目と 2 行目は 修正前 の play
に対して 同じ処理を行う プログラムですが、2 行目 の方が わかりやすい と思いませんか?
mb.play(True, False)
mb.play(circle_ai=True, cross_ai=False)
キーワード引数 の 欠点 は、キーワード を 記述する必要 があるため、実引数の数 が 多くなる と、記述が長くなってしまう 点にあります。先ほどの 修正 は、実引数 を 1 つにまとめる ことで、キーワード引数の記述 を 短くする ための 修正 です。下記のプログラムの 1 行目と 2 行目を比べて、記述の量 と、どちらがわかりやすいか を 確認 して下さい。
mb.play(circle_ai=True, cross_ai=False)
mb.play(is_ai=[True, False])
人によって 何がわかりやすいか は 異なる ので、修正前の方がわかりやすいと思う人がいるかもしれません。そのような場合は、修正前のプログラムを採用して下さい。
ランダムなマスに着手を行う AI
ai1
は簡単に実装できますが、全く強くはありませんし、同じ局面 であれば、毎回 同じ着手しか行わない ので、対戦してもあまり面白くはありません。そこで、次は、簡単に実装できる AI として、下記のようなアルゴリズムで着手を行う AI を実装することにします。
- 空いているマスの中から ランダム に 1 つを 選択する
合法手の計算
一般的に、ゲームでプレイヤーがとることができる行動のことを 合法手(legal move)と呼びます。ランダムなマスに着手を行うためには、合法手 の 一覧 を 計算 する必要があります。合法手の一覧の計算は、今後の AI を 作成 する際にも 必要 になるので、Marubatsu
クラスに 合法手の一覧 を 計算 する メソッドを定義 して利用することにします。
名前:合法手 を 計算(calculate)するので、calc_legal_moves
1という名前にする
処理:合法手の一覧 を 計算 する
入力:なし
出力:合法手は (x, y) という tuple で表現 し、合法手の一覧 を 要素 として持つ list を返す
下記のプログラムは、このメソッドの定義です。
-
2 行目:計算した 合法手の一覧 を 代入 する
legal_moves
という ローカル変数 に 空の list を 代入 して 初期化 する - 3、4 行目:2 重の for 文 によって、すべてのマス に対する 繰り返し処理 を行う
-
5、6 行目:(x, y) のマスが 空であるかどうか を 判定 し、空の場合 は
legal_moves
の要素に、(x, y)
を追加 する -
7 行目:2 重の for 文による 繰り返し処理が終了 した 時点 で、
legal_moves
に 合法手の一覧 が 代入 されているので、それを 返り値として返す
1 def calc_legal_moves(self):
2 legal_moves = []
3 for y in range(self.BOARD_SIZE):
4 for x in range(self.BOARD_SIZE):
5 if self.board[x][y] == Marubatsu.EMPTY:
6 legal_moves.append((x, y))
7 return legal_moves
8
9 Marubatsu.calc_legal_moves = calc_legal_moves
行番号のないプログラム
def calc_legal_moves(self):
legal_moves = []
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
if self.board[x][y] == Marubatsu.EMPTY:
legal_moves.append((x, y))
return legal_moves
Marubatsu.calc_legal_moves = calc_legal_moves
以前の記事 で説明したように、tuple を記述 する際に、基本的 には 外側 の ()
を省略できますが、省略すると 意味が曖昧なる場合 は 省略 することは できません。
上記のプログラムの 6 行目を下記のプログラムのように記述すると、(x, y)
という 1 つの tuple ではなく、x
と y
という 2 つの変数 が実引数に記述されるという 意味になる ため、tuple の 外側の ()
を省略 することは できません。
上記のプログラムを、下記のように記述することはできない。
legal_moves.append(x, y)
下記のプログラムは、calc_legal_moves
を使って、ゲームの 開始直後 に 合法手の一覧を表示 した場合と、(0, 0) に着手を行った直後 に 合法手の一覧を表示 した場合です。実行結果から、2 行目では すべてのマス の座標が、4 行目では (0, 0) を除いたマス の座標が 合法手の一覧 として 計算される ことが確認できます。
mb = Marubatsu()
print(mb.calc_legal_moves())
mb.move(0, 0)
print(mb.calc_legal_moves())
実行結果
[(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
[(1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
list 内包表記の中に if を記述する方法
上記のプログラムは、list 内包表記の中 に if を記述 することで、簡潔に記述 することができます。実際に 良く使われる ので、その方法について説明します。
入れ子になっていない for 文の場合
上記のプログラムは 入れ子 になった 2 重の for 文 が記述されているため 複雑 なので、先に 簡単な、下記のような for 文が 入れ子になっていない例 を説明します。
下記のプログラムは、以下のような処理を行うプログラムです。
-
1 行目:
a
を空の list で初期化する -
2 行目:for 文で、0 から 10 未満までの整数を
i
に代入しながら繰り返し処理を行う -
3、4 行目:
i % 2 == 0
によって、i
を 2 で割った余りが0
である、すなわちi
が偶数であるかどうか を 判定 し、偶数の場合 のみa
の 要素 にi
を追加 する
実行すると、0
から 10
未満の 偶数 を 要素 として持つ list が a
に代入されます。
a = []
for i in range(10):
if i % 2 == 0:
a.append(i)
print(a)
実行結果
[0, 2, 4, 6, 8]
上記のプログラムは、3 行目の if 文がなければ、list 内包表記を使って a = [i for i in range(10)]
のように記述できます。
上記のプログラムのように、for 文の ブロックの中 に if 文が記述 されており、その 条件式が True
の場合 に a
に 要素を追加 する処理を行うプログラムは、下記のプログラムのように list 内包表記 を使って 簡潔に記述 することができます。
a = [i for i in range(10) if i % 2 == 0]
print(a)
実行結果
[0, 2, 4, 6, 8]
一見すると、list 内包表記への変換がややこしく思えるかもしれませんが、以下の手順 を覚えると 簡単 に list 内包表記に 変換できるようになる と思います。
手順 1:for 文と if 文を抜き出す
for i in range(10):
if i % 2 == 0:
手順 2:上記の :
を削除 して 1 行で並べる
for i in range(10) if i % 2 == 0
手順 3:上記 の 先頭 に append
の実引数を記述 し、全体を []
で囲う
[ i for i in range(10) if i % 2 == 0 ]
入れ子になっている for 文の場合
下記は calc_legal_moves
の中の、legal_moves
を計算する部分 のプログラムです。
legal_moves = []
for y in range(self.BOARD_SIZE):
for x in range(self.BOARD_SIZE):
if self.board[x][y] == Marubatsu.EMPTY:
legal_moves.append((x, y))
上記のプログラムような、入れ子 になっている for 文 の場合は、4 行目の if 文がなければ、以前の記事 で説明したように、下記プログラムのような list 内包表記で記述 できます。
legal_moves = [(x, y) for y in range(self.BOARD_SIZE) for x in range(self.BOARD_SIZE)]
4 行目の if 文 を 考慮に入れた場合 は、先程と同様 に、最後 に if の記述を追加 して、下記のプログラムのように記述することができます。なお、長いので途中で改行しました。
この修正も、先ほど 説明した場合と 同様 に、for から if まで の行の :
を削除 し、[
と ]
の中 に 記述 すれば良いので、手順は難しくはないでしょう。
legal_moves = [(x, y) for y in range(self.BOARD_SIZE) for x in range(self.BOARD_SIZE)
if self.board[x][y] == Marubatsu.EMPTY]
下記は、list 内包表記を使って calc_legal_moves
を修正したプログラムです。なお、大部分が修正されているので、修正箇所は省略します。
def calc_legal_moves(self):
legal_moves = [(x, y) for y in range(self.BOARD_SIZE)
for x in range(self.BOARD_SIZE)
if self.board[x][y] == Marubatsu.EMPTY]
return legal_moves
Marubatsu.calc_legal_moves = calc_legal_moves
下記は、修正した calc_legal_moves
が正しく動作するかどうかを確認するプログラムです。実行結果から、正しく動作することが確認できます。
mb = Marubatsu()
print(mb.calc_legal_moves())
mb.move(0, 0)
print(mb.calc_legal_moves())
実行結果
[(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
[(1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
本記事では、この記述を採用しますが、分かりづらいと思った方は 無理に list 内包表記を 使う必要はありません ので、分かりやすいと思ったほうを採用して下さい。
else がある場合の記述方法
今回の記事では利用しませんが、下記のプログラムのように、else が記述 されている場合でも list 内包表記 で 記述 することが できます。なお、下記のプログラムは、奇数 を 負の数に変換 した、0 から 10 未満の整数 を 要素 として持つ list を a
に代入 します。
a = []
for i in range(10):
if i % 2 == 0:
a.append(i)
else:
a.append(-i)
print(a)
実行結果
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9]
このプログラムは、list 内包表記を使って、下記のように記述できます。
else がない場合と異なり、if や else などは、for より前 に記述する必要があります。
a = [i if i % 2 == 0 else -i for i in range(10) ]
print(a)
実行結果
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9]
上記のプログラムで、for の前に記述する i if i % 2 == 0 else -i
の部分は、以前の記事 で説明した 三項演算子 です。
a = []
for 変数 in 反復可能オブジェクト:
if 条件式:
a.append(式1)
else:
a.append(式2)
従って、上記のプログラムは、list 内包表記を使って、下記のように記述できます。
a = [式1 if 条件式 else 式2 for 変数 in 反復可能オブジェクト]
list からのランダムな要素の選択
Python には、ランダムな処理 を行うための random
という モジュール があり、その中に choice
という、反復可能オブジェクト の 要素の中 から ランダム に 一つ選択(choice)した 要素を返す という 関数が定義 されています。この choice
を使うことによって、合法手の一覧の中 から ランダム に 合法手を 1 つ選択 することができます。
下記は、legal_moves
の要素をランダムに一つ選択して表示するプログラムです。
-
1 行目:
random
モジュール からchoice
関数 を インポート する -
3 行目:
legal_moves
に 合法手の一覧 を 代入 する -
4 行目:
choice
を使って、legal_moves
の 要素 を ランダムに選択 して 表示 する
from ramdom import choice
legal_moves = mb.calc_legal_moves()
print(choice(legal_moves))
実行結果(実行結果はランダム なので 下記とは異なる場合 があります)
(1, 0)
random
モジュールの詳細については、下記のリンク先を参照して下さい。
choice
の詳細については、下記のリンク先を参照して下さい。
ai2
の定義
合法手の一覧の中からランダムな合法手を選択する方法が分かったので、ai2
は以下のようなプログラムで定義することができます。
def ai2(mb):
legal_moves = mb.calc_legal_moves()
return choice(legal_moves)
下記のプログラムを実行することで、ゲーム開始直後に、ai2
を使って ランダムな合法手 を 表示 することができます。
mb = Marubatsu()
print(ai2(mb))
実行結果(実行結果はランダムなので下記とは異なる場合があります)
(0, 0)
play
メソッドの改良
先程定義した play
メソッドは、〇 と × を ai1
が担当するかどうか を選択できるように実装したので、ai2
が担当するかどうか を選択できるようにするためには、play
メソッドの下記の 4 行目を x, y = ai2(self)
のように修正する必要があります。
# 〇の手番で、ai_circle が True の場合は ai が着手を行う
if (self.turn == Marubatsu.CIRCLE and is_ai[0]) or \
(self.turn == Marubatsu.CROSS and is_ai[1]):
x, y = ai1(self)
しかし、そのように修正すると、今度は play
メソッドで ai1
を選択できなく なってしまいます。そこで、play
メソッドを 改良 し、〇 と × の着手を 人間 または、指定した AI の処理を行う関数 が担当するかどうかを 選択できる ように修正することにします。
以前の記事で説明したように、Python では、変数に関数を代入 することができます。また、関数を代入した変数 は、関数と同じ機能を持つ ようになるため、下記のプログラム2の 5 行目のように、関数呼び出し を行うことができるようになります。
def add(x, y):
return x + y
a = add
print(a(1, 2))
実行結果
3
この性質から、play
メソッドの 仮引数 に、AI の処理 を行う 関数を代入 することで、play
メソッドの ブロックの中 で 指定した AI の 処理を行う関数 を 呼び出す ことができるようになります。その際に、人間 が 手番を担当 する場合の データ を どのように記述するか を 決める 必要があります。本記事では、人間 が手番を 担当 するということは、AI が 存在しない ことなので、データが存在しない ことを表す None
で表現 することにします。
下記は、そのように play
メソッドを修正したプログラムです。
-
1 行目:仮引数 が、AI の処理 を行う 関数を代入 するように 変更 されたので、仮引数の名前 を名前を
ai
に 変更 する -
9、10 行目:〇 の手番 で、なおかつ、人間の担当ではない(
ai[0] is not None
)場合は、ai[0]
に AI の処理 を行う 関数が代入 されているので、ai[0](self)
を呼び出して 着手を計算 し、その座標をx
とy
に代入 する -
11、12 行目:× の手番 で、なおかつ、人間の担当ではない(
ai[1] is not None
)場合に、9、10 行目と 同様の処理 を行う
1 def play(self, ai):
2 # 〇×ゲームを再起動する
3 self.restart()
4 # ゲームの決着がついていない間繰り返す
5 while self.status == Marubatsu.PLAYING:
6 # ゲーム盤の表示
7 print(self)
8 # ai が着手を行うかどうかを判定する
9 if self.turn == Marubatsu.CIRCLE and ai[0] is not None:
10 x, y = ai[0](self)
11 elif self.turn == Marubatsu.CROSS and ai[1] is not None:
12 x, y = ai[1](self)
13 else:
14 # キーボードからの座標の入力
以下同じなので省略
行番号のないプログラム
def play(self, ai):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# ai が着手を行うかどうかを判定する
if self.turn == Marubatsu.CIRCLE and ai[0] is not None:
x, y = ai[0](self)
elif self.turn == Marubatsu.CROSS and ai[1] is not None:
x, y = ai[1](self)
else:
# キーボードからの座標の入力
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, is_ai):
+def play(self, ai):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# ai が着手を行うかどうかを判定する
- if (self.turn == Marubatsu.CIRCLE and is_ai[0]) or \
- (self.turn == Marubatsu.CROSS and is_ai[1]):
- x, y = ai(self)
+ if self.turn == Marubatsu.CIRCLE and ai[0] is not None:
+ x, y = ai[0](self)
+ elif self.turn == Marubatsu.CROSS and ai[1] is not None:
+ x, y = ai[1](self)
else:
# キーボードからの座標の入力
以下同じなので省略
改良した play
メソッドの動作の確認
play
メソッドが 正しく動作するか どうかを 確認 するために、人間、ai1
、ai2
の すべての組み合わせ で 対戦 を行うことにします。以下、対戦の組み合わせ を、〇 の担当 を 先に記述 して、ai
VS ai1
のように 表記 することにします。
ai1
VS ai1
下記は、ai1
VS ai1
で対戦を行うプログラムです。ai1
は ランダムな着手 は 行わない ので、常に 対戦 結果 は 同じ になります。そのことを確認したい方は、下記のプログラムを 何度か実行 し、実行結果 が 常に同じになる ことを 確認 して下さい。
mb.play(ai=[ai1, ai1])
実行結果
Turn o
...
...
...
Turn x
O..
...
...
Turn o
oX.
...
...
Turn x
oxO
...
...
Turn o
oxo
X..
...
Turn x
oxo
xO.
...
Turn o
oxo
xoX
...
winner o
oxo
xox
O..
ai1
VS ai2
下記は、ai1
VS ai2
で対戦を行うプログラムです。ai2
は ランダムな着手 を行うので、毎回 対戦 結果 は 変わります。一方、ai1
は、必ず 左上のマスから 順番 に 空いているマスを探し、最初に見つかったマス に 着手 を行います。下記の実行結果では、実際に そのような方針 で 〇 を担当 する ai1
が 着手を行っている ことが 確認 できます。
mb.play(ai=[ai1, ai2])
実行結果(実行結果はランダムなので下記とは異なる場合があります)
Turn o
...
...
...
Turn x
O..
...
...
Turn o
o..
..X
...
Turn x
oO.
..x
...
Turn o
ooX
..x
...
Turn x
oox
O.x
...
Turn o
oox
o.x
X..
Turn x
oox
oOx
x..
Turn o
oox
oox
xX.
winner o
oox
oox
xxO
その他の対戦
下記は、残りの組み合わせ で 対戦を行う プログラムです。実行結果 は長くなるので 省略 しますが、以下の点に注目 して 実際に何度か対戦 してみて下さい。
-
ai2
VSai1
:〇 と × の 担当が入れ替わった 点を除けば、ai1
VSai2
の場合と 同様 -
ai2
VSai2
:すべて の着手で ランダムな着手 が行われる -
人間
VSai1
、ai1
VS人間
:ai1
の着手が、ai1
のアルゴリズムで行われる -
人間
VSai2
、ai2
VS人間
:ai2
の着手が、ランダムに行われる -
人間
VS人間
:人間どうしの対戦が正しく行われる
mb.play(ai=[ai2, ai1])
mb.play(ai=[ai2, ai2])
mb.play(ai=[None, ai1])
mb.play(ai=[ai1, None])
mb.play(ai=[None, ai2])
mb.play(ai=[ai2, None])
mb.play(ai=[None, None])
play
メソッドの if 文の処理をまとめる
先程の play
メソッドの下記の部分は、2、3 行目 と 4、5 行目 に 同じような処理 が記述されているので、冗長 だと思いませんか?
1 # ai が着手を行うかどうかを判定する
2 if self.turn == Marubatsu.CIRCLE and ai[0] is not None:
3 x, y = ai[0](self)
4 elif self.turn == Marubatsu.CROSS and ai[1] is not None:
5 x, y = ai[1](self)
6 else:
7 略
2、3 行目と 4、5 行目の 違い は、前半 の self.turn ==
の部分と、その後 の ai[0]
、ai[1]
の部分です。ai[0]
と ai[1]
の 違い は、インデックスの番号 で、その 番号 は 前半 の self.turn ==
の部分 で決まる ので、下記のプログラムのように、先に インデックスの 番号 を if 文で計算する ことで、プログラムを まとめる ことができます。
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
x, y = ai[index](self)
else:
略
下記のプログラムはそのように play
メソッドを修正したプログラムです。
def play(self, ai):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
x, y = ai[index](self)
else:
# キーボードからの座標の入力
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, ai):
# 〇×ゲームを再起動する
self.restart()
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# ゲーム盤の表示
print(self)
# ai が着手を行うかどうかを判定する
- if self.turn == Marubatsu.CIRCLE and ai[0] is not None:
- x, y = ai[0](self)
- elif self.turn == Marubatsu.CROSS and ai[1] is not None:
- x, y = ai[1](self)
# 現在の手番を表す ai のインデックスを計算する
+ index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ai が着手を行うかどうかを判定する
+ if ai[index] is not None:
+ x, y = ai[index](self)
else:
# キーボードからの座標の入力
以下同じなので省略
本当は play
メソッドを修正したので正しく動作するかどうかを確認する必要がありますが、長くなるので省略します。余裕がある人は各自で確認しておいてください。
ai1
の別の実装方法
ai1
は、calc_legal_moves
を使って、下記のプログラムのように 簡潔に記述 できます。
calc_legal_moves
は、合法手の一覧 を、ai1
と同様 に、左上のマス から 順番に調べている ので、見つかった 合法手の一覧 の 先頭 の 合法手 が、ai1
が 選択する合法手 になります。従って、下記のプログラムの 3 行目のように、合法種の一覧 を表す list の 先頭の要素 である legal_moves[0]
を 返す ことで ai1
を実装できます。
def ai1(mb):
legal_moves = mb.calc_legal_moves()
return legal_moves[0]
なお、元の ai1
は、空のマス が 見つかった時点 で 処理が終了 しますが、上記のプログラムは、すべての合法手を見つけてから 、その先頭の 合法手を返す 処理を行うので、上記のプログラムの方が、元の ai1
よりも 処理に時間がかかる という 欠点 があります。
ただし、ai1
の アルゴリズム の 処理にかかる時間 は 短い ので、どちらを選択しても 大きな違いはない ので、どちらを選んでも良いと思います。
本記事では元の ai1
の定義を採用することにします。
今回の記事のまとめ
今回の記事では、簡単に実装 できる AI の処理 を行う 関数 を 2 種類実装 し、play
メソッドを 改良 して、〇 と × を、人間 または 任意の AI が担当 できるように 改良 しました。
作成した AI の アルゴリズム は以下の通りです。
関数名 | アルゴリズム |
---|---|
ai1 |
左上から順に空いているマスを探し、最初に見つかったマスに着手する |
ai2 |
ランダムなマスに着手する |
今回作成した AI はどちらも 非常に弱い ので、次回の記事からは、どのようにすれば 強い AI を 実装 できるかについて説明します。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
AI に関連するプログラムは、Marubatsu
クラスとは 別の処理 を行うので、ai.py に記述する ことにします。以下のリンクは、今回の記事で作成した ai.py です。
次回の記事