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?

Pythonで〇×ゲームのAIを一から作成する その39 単純なAIの作成

Last updated at Posted at 2023-12-24

目次と前回の記事

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

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

前回までのおさらい

前回の記事で、〇×ゲームを遊ぶための play メソッドの実装が完了しました。

今回の記事からいよいよ 〇×ゲームの AI の作成を開始します。

〇×ゲームの AI の処理を行う関数

〇×ゲームの AI が行う処理 は、〇×ゲームの 局面 から、次に 着手 を行う マスの座標を計算 するというものです。これから、さまざまな〇×ゲームのAI を作成することになるので、作成する AI ごと に、下記のような処理を行う 関数を定義 することにします。

処理:〇×ゲームの 局面 から、次に 着手 を行う マスの座標計算 する
名前:それぞれの AI ごとai1ai2 のような 名前をつける
入力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. 左上のマスから順番に 空いているマス探す
  2. 最初に見つかった 空いている マス を着手を行うマスとして 選択 する

上記の手順 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_circleTrue の場合に 10 行目で ai1 を呼び出し、その 返り値 を展開して 着手 を行う 座標xy に代入 する
  • 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_crossTrue の場合を判定するように条件式を修正する

なお、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_circleai_cross によって それぞれのマーク を人間か ai1どちらが担当 するかを 判定 していましたが、これを 1 つの仮引数まとめる ことができます。その場合は、list を使って 〇 と × を どちらが担当 するかを 指定 し、list の 0 番の要素 を、1 番の要素×ai1担当するかどうか を表します。

下記は、そのように play メソッドを修正したプログラムです。

  • 1 行目仮引数is_ai のみ に修正する
  • 9、10 行目is_ai[0] を、ai[1]× を、ai1担当するかどうか表す ので、ai_circleis_ai[0] に、ai_crossis_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:
            # キーボードからの座標の入力
以下同じなので省略

実行結果は省略しますが、下記のプログラムで、人間と ai14 通りの組み合わせ で対戦を行うことができることを確認できます。

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_moves1という名前にする
処理合法手の一覧計算 する
入力:なし
出力:合法手は (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 ではなくxy という 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 未満の 偶数要素 として持つ lista に代入されます。

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 未満の整数要素 として持つ lista に代入 します。

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) を呼び出して 着手を計算 し、その座標を xy に代入 する
  • 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 メソッドが 正しく動作するか どうかを 確認 するために、人間、ai1ai2すべての組み合わせ対戦 を行うことにします。以下、対戦の組み合わせ を、〇 の担当先に記述 して、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 VS ai1:〇 と × の 担当が入れ替わった 点を除けば、ai1 VS ai2 の場合と 同様
  • ai2 VS ai2すべて の着手で ランダムな着手 が行われる
  • 人間 VS ai1ai1 VS 人間ai1 の着手が、ai1 のアルゴリズムで行われる
  • 人間 VS ai2ai2 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 です。

次回の記事

  1. 何かを計算する関数の名前の一部として、calculate の略である calc が良く使われます

  2. 以前の記事では sum という変数に add を代入していましたが、sum という名前の組み込み関数が存在するので、上記のプログラムでは a に代入しています

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?