LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その35 さまざまな勝利判定のアルゴリズム2

Last updated at Posted at 2023-12-10

目次と前回の記事

前回までのおさらい

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

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

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

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

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

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

前回までのおさらい

前回の記事では、〇 または × が勝利していることを判定する is_winner メソッドを実装するさまざまなアルゴリズムを紹介しました。今回の記事ではその続きを紹介します。

is_winner のアルゴリズム その 6 (あらかじめデータを作成しない)

前回の 記事の最後では、下記のプログラムのように、最初 に一直線上のマスの座標を作成するための データを集めた list作成 し、その データを元繰り返し処理 によって 判定 を行いました。

この方法には、あらかじめ データを 作成 しておくことによって、判定 を 1 つの 繰り返し処理 によって まとめて行う ことができるという 利点 があります。

    def is_winner(self, player):
        # 一直線上のマスの座標を作成するためのデータを集めた list を代入する変数を空の list で初期化する
        judge_data_list = []
        # 横方向の座標を作成するために必要なデータを追加する
        for y in range(self.BOARD_SIZE):
            judge_data_list.append({ "coord": [0, y], "dx": 1, "dy": 0 })
        # 縦方向の座標を作成するために必要なデータを追加する
        for x in range(self.BOARD_SIZE):
            judge_data_list.append({ "coord": [x, 0], "dx": 0, "dy": 1 })
        # 左上から右下方向の座標を追加する
        judge_data_list.append({ "coord": [0, 0], "dx": 1, "dy": 1 })
        # 右上から左下方向の座標を追加する
        judge_data_list.append({ "coord": [2, 0], "dx": -1, "dy": 1 })     

        # 一直線上のマスの座標作成するためのデータを順番に取り出す繰り返し処理
        for judge_data in judge_data_list:
            # 取り出した一直線上のマスに player が配置されているかどうかを判定する
            if self.is_same(player, **judge_data):
                # 並んでいれば player の勝利なので True を返す
                return True

        # どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
        return False

上記のプログラムでは、is_winner実行する際 に、横方向で 3 つ、縦方向で 3 つ、斜め方向で 2 つの、計 8 つ要素 を持つ judge_data_list作成 していますが、この方法では、ゲーム盤の サイズが大きくなった 際に、前回の記事 で紹介したその 4 の方法ほどではありませんが、多くの要素 を持つ judge_data_list作成する必要 があります。

judge_data_list のような list を 作成せず に、上記と同様の処理 を行う方法として、下記のプログラムのように、横方向、縦方向、斜め方向の それぞれに分けて for 文による 繰り返し処理を行う という アルゴリズム が考えられます。なお、修正箇所が多すぎるので、修正箇所は示しません。

from marubatsu import Marubatsu

def is_winner(self, player):
    # 横方向の判定
    for y in range(self.BOARD_SIZE):
        if self.is_same(player, coord=[0, y], dx=1, dy=0):
            return True
    # 縦方向の判定
    for x in range(self.BOARD_SIZE):
        if self.is_same(player, coord=[x, 0], dx=0, dy=1):
            return True
    # 左上から右下方向の判定
    if self.is_same(player, coord=[0, 0], dx=1, dy=1):
        return True
    # 右上から左下方向の判定
    if self.is_same(player, coord=[2, 0], dx=-1, dy=1):
        return True

    # どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
    return False

Marubatsu.is_winner = is_winner

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果から、すべてのテストケースで期待された処理が行われることが確認できます。

from test import test_judge

test_judge()

実行結果

Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

このアルゴリズムの性質

この方法では、判定を行うための、self.is_samereturn True4 箇所に記述 する必要があるので 効率が悪い ように 思える かもしれませんが、その 5 のプログラムで記述していた、4 つjudge_data_list.append によって 要素を追加 する処理が代わりに なくなる ので、結局 のところ、記述する プログラムの量ほぼ同じ です。実際 に、コメントを除いた 場合の is_winnerブロックの行数 は その 5 も その 6 も 11 行変わりません

下記プログラムは、その 5 と その 6 の 横方向の判定関する部分抜き出した ものです。それぞれの部分が どのように対応 しているか 確認 して下さい。

# その 5 の横方向の座標を作成するために必要なデータを追加する処理
for y in range(self.BOARD_SIZE):
    judge_data_list.append({ "coord": [0, y], "dx": 1, "dy": 0 })

# その 6 の横方向の判定を行う処理
for y in range(self.BOARD_SIZE):
    if self.is_same(winner, coord=[0, y], dx=1, dy=0):
        return True

その 6 の他の 利点 としては、もちろん人によって 意見は異なる可能性 はありますが、その 6 のプログラムの方が、プログラムの 見た目がわかりやすい という利点もあります。

プログラムの簡略化

先程のプログラムは、下記のプログラムのように、横方向縦方向判定1 つ の for 文で まとめて記述 することもできます。なお、修正前の for 文の 変数名xy でしたが、そのままではプログラムの意味が分かりづらくなるので、変数名を i に変更 しました。

def is_winner(self, player):
    # 横方向と縦方向の判定
    for i in range(self.BOARD_SIZE):
        if self.is_same(player, coord=[0, i], dx=1, dy=0) or \
           self.is_same(player, coord=[i, 0], dx=0, dy=1):
            return True
    # 左上から右下方向の判定
    if self.is_same(player, coord=[0, 0], dx=1, dy=1):
        return True
    # 右上から左下方向の判定
    if self.is_same(player, coord=[2, 0], dx=-1, dy=1):
        return True

    # どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
    return False

Marubatsu.is_winner = is_winner
修正箇所
def is_winner(self, player):
-   # 横方向の判定
+   # 横方向と縦方向の判定
-   for y in range(self.BOARD_SIZE):
+   for i in range(self.BOARD_SIZE):
-       if self.is_same(player, coord=[0, y], dx=1, dy=0):
+       if self.is_same(player, coord=[0, i], dx=1, dy=0) or \
+          self.is_same(player, coord=[i, 0], dx=0, dy=1):
            return True
-   # 縦方向の判定
-   for x in range(self.BOARD_SIZE):
-       if self.is_same(player, coord=[x, 0], dx=0, dy=1):
-           return True
    # 左上から右下方向の判定
    if self.is_same(player, coord=[0, 0], dx=1, dy=1):
        return True
    # 右上から左下方向の判定
    if self.is_same(player, coord=[2, 0], dx=-1, dy=1):
        return True

    # どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
    return False

Marubatsu.is_winner = is_winner

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

is_same のアルゴリズム その 2 (数を数える)

次は、is_same のさまざまなアルゴリズムを紹介します。

下記のプログラムは、前回の記事で定義した is_same メソッドです。前回の記事ではそのように記述しませんでしたが、下記のプログラムを is_same のアルゴリズム その 1 と考えて下さい。

    def is_same(self, mark, coord, dx, dy):
        x, y = coord   
        for _ in range(self.BOARD_SIZE):
            if self.board[x][y] != mark:
                return False
            x += dx
            y += dy

        return True

上記のプログラムでは、一直線上すべてのマス に、mark のマークが 配置されているかどうか を判定していますが、下記のような、一直線上のマスに、mark のマークが 配置 されている マスの数数える というアルゴリズムでも、同様の判定を行うことができます。

なお、「mark のマークが配置」は冗長なので、以後は「mark が配置」のように記述します。

  1. mark が配置 されている マスの数数える
  2. 数えたマスの数 が、一直線上のマスの数同じ であれば、mark一直線上に並んでいる ので True を返す
  3. そうでなければ 一直線上に 並んでいない ので False を返す

〇× ゲームの場合は、一直線上の 3 つのマスmarkいくつ配置されているか数え3 つ であれば、一直線上に 並んでおりそれ以外 の場合は 並んでいない ことになります。

下記は、このアルゴリズムで is_same を記述したプログラムで、修正点は以下の通りです。

  • 3 行目mark が配置された マスの数を数える 変数 count0 で初期化 する
  • 5、6 行目(x, y) のマスに mark配置されている 場合は、count1 を加算 する
  • 10 行目count と、一直線上の マスの数 を表す self.BOARD_SIZE== 演算子で比較 した 計算結果を返す。その結果、この関数は、countself.BOARD_SIZE等しければ True を、そうでなければ False を返すようになる
 1  def is_same(self, mark, coord, dx, dy):
 2      x, y = coord   
 3      count = 0
 4      for _ in range(self.BOARD_SIZE):
 5          if self.board[x][y] == mark:
 6             count += 1
 7          x += dx
 8          y += dy
 9
10      return count == self.BOARD_SIZE
11
12  Marubatsu.is_same = is_same
行番号のないプログラム
def is_same(self, mark, coord, dx, dy):
    x, y = coord   
    count = 0
    for _ in range(self.BOARD_SIZE):
        if self.board[x][y] == mark:
            count += 1
        x += dx
        y += dy

    return count == self.BOARD_SIZE

Marubatsu.is_same = is_same
修正箇所
def is_same(self, mark, coord, dx, dy):
    x, y = coord   
+   count = 0
    for _ in range(self.BOARD_SIZE):
-       if self.board[x][y] != mark:
+       if self.board[x][y] == mark:
-           return False
+           count += 1
        x += dx
        y += dy

-   return True
+   return count == self.BOARD_SIZE

Marubatsu.is_same = is_same

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

このアルゴリズムの性質

その 1 のアルゴリズムでは、for 文の 繰り返しの処理の中 で、(x, y) のマスに mark配置されていない ことが 分かった時点return 文 によって for 文の 繰り返しの処理の途中 であっても 関数の処理が終了 します。一方、その 2 のアルゴリズムは、mark が配置される マスを数えている ため、for 文が 途中で終了 することは ありません。そのため、その 2 のアルゴリズムは、その 1 のアルゴリズムよりも、平均 すると 処理に時間がかかる という欠点があります。

そのため、計算速度の面 だけを考えれば、その 1 のアルゴリズムの方 が、その 2 のアルゴリズムよりも 優秀 です。それにも関わらず、その 2 のアルゴリズムを紹介したのは、今後 AI を実装する際に、一直線上に、指定した マークいくつ配置されているか計算する場面 があるからです。

また、〇×ゲームでは必要はありませんが、五目並べのような、一直線上のどこでも良いので、同じマスに 連続して 同じ石が いくつ並んでいるかを数える 必要があるような場合は、その 1 のアルゴリズム を使うことは できません

is_same のアルゴリズム その 3 (list と sumの利用)

その 2 のアルゴリズムを、listsum という組み込み関数を使って 記述 できます。

list の作成と sum による計算

まず、以下のような性質を持つ list を作成 します。

  • list の要素 は、is_same調べる それぞれの マスに対応 する
  • 要素 は、is_same で調べるマスに mark が配置されていれば 1そうでなければ 0 とする

上記のような list を作成することができれば、list の 要素の合計 を計算することで、is_same が調べるマスのうち、mark が配置 されている マスの数数える ことができます。

Python では、sum という 組み込み関数 を利用することで、下記のプログラムのように、実引数に記述 した、反復可能オブジェクトすべての要素の合計計算 することができます。

print(sum([1, 2, 3]))

実行結果

6

sum の詳細については、下記のリンク先を参照してください。

従って、is_same を、下記のプログラムのように記述できます。修正点は以下の通りです。

  • 3 行目same_list空の list を代入して 初期化 する
  • 5 ~ 8 行目:(x, y) のマスに mark が配置 されている場合は 0 を、そうでなければ 1same_list要素として追加 する
  • 12 行目sum を使って、same_list要素の合計計算 し、self.BOARD_SIZE等しいかどうか を比較した 計算結果返り値 として 返す
 1  def is_same(self, mark, coord, dx, dy):
 2      x, y = coord
 3      same_list = []
 4      for _ in range(self.BOARD_SIZE):
 5          if self.board[x][y] == mark:
 6              same_list.append(1)
 7          else:
 8              same_list.append(0)
 9          x += dx
10          y += dy
11  
12      return sum(same_list) == self.BOARD_SIZE
13
14  Marubatsu.is_same = is_same
行番号のないプログラム
def is_same(self, mark, coord, dx, dy):
    x, y = coord
    same_list = []
    for _ in range(self.BOARD_SIZE):
        if self.board[x][y] == mark:
            same_list.append(1)
        else:
            same_list.append(0)
        x += dx
        y += dy

    return sum(same_list) == self.BOARD_SIZE

Marubatsu.is_same = is_same
修正箇所
def is_same(self, mark, coord, dx, dy):
    x, y = coord
-   count = 0
+   same_list = []
    for _ in range(self.BOARD_SIZE):
        if self.board[x][y] == mark:
-           count += 1
+           same_list.append(1)
+       else:
+           same_list.append(0)
        x += dx
        y += dy

-   return count == self.BOARD_SIZE
+   return sum(same_list) == self.BOARD_SIZE

Marubatsu.is_same = is_same

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

上記のプログラムは、いくつかの方法で 簡略化 できるので、その方法について説明します。

sum に関する注意点

sum を利用する際は、反復可能オブジェクトの 要素 は、すべて数値型のデータ でなければならない点に 注意が必要 です。例えば、下記のプログラムは、list の 要素文字列が代入 されているので実行すると エラーが発生 します。

print(sum(["1", "2", "3"]))

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\035\marubatsu.ipynb セル 10 line 1
----> 1 print(sum(["1", "2", "3"]))

TypeError: unsupported operand type(s) for +: 'int' and 'str'

上記のエラーメッセージは、以下のような意味を持ちます。

  • TypeError
    データ型(type)に関するエラー
  • unsupported operand type(s) for +: 'int' and 'str'
    整数型(int type)と文字列型(str type)に対する(for)+ 演算子(operand)による演算は提供されていない(unsupported)

上記のプログラムでは、list の 要素すべて文字列 なので、エラーメッセージの、「整数型と文字列型 に対する + 演算子(operand)による演算」の部分が おかしい のではないかと 思う人がいるかもしれない ので補足します。

sum のような、組み込み関数の定義見ることはできません が、おそらく下記のプログラムのような定義になっているのではないかと 推測されます。なお、下記のプログラムは、本物sum の定義 ではなく疑似的(pseudo)なものなので、関数の名前を pseudo_sum としました。

def pseudo_sum(data):
    total = 0
    for num in data:
        total += num
    return total

合計を計算するためには、途中の計算結果記憶 しておく 変数が必要 なので、上記のプログラムでは、2 行目で total という変数を 0 で初期化 し、4 行目で totallist から取り出したデータ加算 するという処理を行っています。data["1", "2", "3"] のようなデータが代入されていた場合は、3 行目の for 文の 最初の繰り返し で、num"1" が代入 されるため、4 行目では、0 + "1" という、整数型と文字列型 のデータを + 演算子で 演算 するという処理が行われるため、上記のようなエラーが発生します。

文字列型 のデータは、反復可能オブジェクト ですが、同様の理由 で、sum実引数文字列型のデータを記述 して呼び出すと、下記のプログラムのように エラーが発生 します。

print(sum("123"))

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\035\marubatsu.ipynb セル 12 line 1
----> 1 print(sum("123"))

TypeError: unsupported operand type(s) for +: 'int' and 'str'

三項演算子による簡略化

        if self.board[x][y] == mark:
            same_list.append(1)
        else:
            same_list.append(0)

先程のプログラムの上記の部分は、以前の記事 で説明した 三項演算子 を使うことで、下記のプログラムのように 1 行で記述 することができます。

same_list.append(1 if self.board[x][y] == mark else 0)

下記は、そのように修正したプログラムです。

def is_same(self, mark, coord, dx, dy):
    x, y = coord
    same_list = []
    for _ in range(self.BOARD_SIZE):
        same_list.append(1 if self.board[x][y] == mark else 0)
        x += dx
        y += dy

    return sum(same_list) == self.BOARD_SIZE

Marubatsu.is_same = is_same
修正箇所
def is_same(self, mark, coord, dx, dy):
    x, y = coord
    same_list = []
    for _ in range(self.BOARD_SIZE):
-       if self.board[x][y] == mark:
-           same_list.append(1)
-       else:
-           same_list.append(0)
+       same_list.append(1 if self.board[x][y] == mark else 0)
        x += dx
        y += dy

    return sum(same_list) == self.BOARD_SIZE

Marubatsu.is_same = is_same

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

論理型(TrueFalse)と数値型のデータの計算

上記では、三項演算子を使ってプログラムを簡略化しましたが、この三項演算子省略できます

Python では、論理型(bool)のデータである、TrueFalse数値型 のデータを 直接 +- などの 演算子演算 することができ、その場合は、下記のプログラムのように True1False0みなされて計算 が行われます。

print(True + 2)     # 1 + 2 と同じ
print(False * 3)    # 0 * 3 と同じ
print(True - False) # 1 - 0 と同じ

実行結果

3
0
1

従って、下記のプログラムの 1 行目 と、2 行目全く同じ計算 が行われます。

1 if self.board[x][y] == mark else 0
True if self.board[x][y] == mark else False
self.board[x][y] == mark

また、True if self.board[x][y] == mark else False という式は、self.board[x][y] == mark計算結果True の場合は True を、False の場合は False を計算する式なので、上記のプログラムの 2 行目3 行目全く同じ計算 が行われます。

従って、上記のプログラムの 1 行目の式は、3 行目のように簡潔に記述することができます。下記は、そのように is_same を記述したプログラムです。

def is_same(self, mark, coord, dx, dy):
    x, y = coord
    same_list = []
    for _ in range(self.BOARD_SIZE):
        same_list.append(self.board[x][y] == mark)
        x += dx
        y += dy

    return sum(same_list) == self.BOARD_SIZE

Marubatsu.is_same = is_same
修正箇所
def is_same(self, mark, coord, dx, dy):
    x, y = coord
    same_list = []
    for _ in range(self.BOARD_SIZE):
-       same_list.append(1 if self.board[x][y] == mark else 0)
+       same_list.append(self.board[x][y] == mark)
        x += dx
        y += dy

    return sum(same_list) == self.BOARD_SIZE

Marubatsu.is_same = is_same

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

このアルゴリズムの性質

このアルゴリズムは、このままでは、その 2 のプログラムと比べて 短く記述できていません が、次で説明をする、list 内包表記 を使うことで、プログラムを 簡潔に記述できる ようになります。

なお、三項演算子 は、式が複雑 になって見た目が わかりづらくなる傾向 があります。上記で紹介したような、式の中に 三項演算子のみ を記述するような場合は 問題はありません が、式が 複雑になりすぎると思った場合使わないほうが良い でしょう。

is_same のアルゴリズム その 4 (list 内包表記などを使った簡略化)

その 3 のプログラムは、list 内包表記 などを使って、さらに 簡略化 することができます。

list 内包表記を使った簡略化

以前の記事で説明したように、list 内包表記 は、下記のような for 文の ブロックの中append処理のみ を行うプログラムを、その下のプログラムのように、簡潔に記述 する表記方法です。

a = []
for 変数名 in 反復可能オブジェクト:
    a.append()
a = [ for 変数名 in 反復可能オブジェクト]

先程のプログラムは、下記の for 文による 繰り返し処理ブロックの中 に、x += dx と、y + dy という 式が記述 されているため、list 内包表記 を使って 記述 することが できません

    same_list = []
    for _ in range(self.BOARD_SIZE):
        same_list.append(self.board[x][y] == mark)
        x += dx
        y += dy

従って、上記のプログラムを list 内包表記で記述 するためには、for 文の ブロックの中 から、x += dxy += dy削除する必要 があります。そのためには、この 2 つの式使わず に、same_list.append(self.board[x][y] == mark) の部分を 計算する ようにプログラムを 修正する必要 があります。その方法について少し考えてみて下さい。

same_list.append(self.board[x][y] == mark)xy は、for 文の 繰り返しの処理 の中で、繰り返しのたびdxdy ずつ加算 されていきます。従って、初回 の繰り返しを 0 回目 とした場合、i 回目 の繰り返し処理では、xy の値 は、最初の値から それぞれ dx * idy * i だけ加算 された値になることがわかります。

for 文の 反復可能オブジェクトrange(式) を記述 した場合、for 文の 変数に代入 されるのは、繰り返しの回数 なので、上記の for 文は、下記のプログラムのように修正することができます。

  • 2 行目:for 文の中の 変数名_ から i に修正 する
  • 3 行目xx + i * dxyy + i * dy修正 する
    same_list = []
    for i in range(self.BOARD_SIZE):
        same_list.append(self.board[x + i * dx][y + i * dy] == mark)

for 文の ブロックの中 が、same_list.append だけ になったので、上記のプログラムを下記のプログラムのように list 内包表記 を使って 1 行で記述 することができます。

same_list = [self.board[x + i * dx][y + i * dy] == mark for i in range(self.BOARD_SIZE)

下記は、そのように is_same を記述したプログラムです。

def is_same(self, mark, coord, dx, dy):
    x, y = coord
    same_list = [self.board[x + i * dx][y + i * dy] == mark for i in range(self.BOARD_SIZE)]
    return sum(same_list) == self.BOARD_SIZE

Marubatsu.is_same = is_same
修正箇所
def is_same(self, mark, coord, dx, dy):
    x, y = coord
-   same_list = []
+   same_list = [self.board[x + i * dx][y + i * dy] == mark for i in range(self.BOARD_SIZE)]
-   for _ in range(self.BOARD_SIZE):
-       same_list.append(self.board[x][y] == mark)
-       x += dx
-       y += dy

    return sum(same_list) == self.BOARD_SIZE

Marubatsu.is_same = is_same

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

上記のような 長い list 内包表記 は見た目が わかりづらくなる ので、下記のプログラムのように、for の直前など改行 して 読みやすくする ことが良く行われます。なお、[] の間 であれば、改行の前に \ を記述 する必要や、インデントを揃える 必要は ありません

def is_same(self, mark, coord, dx, dy):
    x, y = coord
    same_list = [self.board[x + i * dx][y + i * dy] == mark
                 for i in range(self.BOARD_SIZE)]
    return sum(same_list) == self.BOARD_SIZE

Marubatsu.is_same = is_same
修正箇所
def is_same(self, mark, coord, dx, dy):
    x, y = coord
-   same_list = [self.board[x + i * dx][y + i * dy] == mark for i in range(self.BOARD_SIZE)]
+   same_list = [self.board[x + i * dx][y + i * dy] == mark
+                for i in range(self.BOARD_SIZE)]
    return sum(same_list) == self.BOARD_SIZE

Marubatsu.is_same = is_same

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

さらなる簡略化とその利点と欠点

上記のプログラムは、一旦 same_list計算した list を代入 していましたが、下記のプログラムのように、sum の実引数直接 list を計算する list 内包表記を記述 することもできます。ただし、この簡略化は、return 文に記述される 式が複雑 になりすぎており、何の処理が行われているか が非常に 分かりづらくなる という デメリット が発生しています。例えば、下記の return 文には == 演算子2 つ記述 されていますが、それぞれが 何を比較 しているかすぐにわかりますか?

def is_same(self, mark, coord, dx, dy):
    x, y = coord
    return sum([self.board[x + i * dx][y + i * dy] == mark
                for i in range(self.BOARD_SIZE)]) == self.BOARD_SIZE

Marubatsu.is_same = is_same
修正箇所
def is_same(self, mark, coord, dx, dy):
    x, y = coord
-   same_list = [self.board[x + i * dx][y + i * dy] == mark
-                for i in range(self.BOARD_SIZE)]
-   return sum(same_list) == self.BOARD_SIZE
+   return sum([self.board[x + i * dx][y + i * dy] == mark
+               for i in range(self.BOARD_SIZE)]) == self.BOARD_SIZE

Marubatsu.is_same = is_same

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

プログラムが わかりづらくなる と、バグが発生しやすくなる だけでなく、バグが発生した時の 修正も困難 になるので、簡略化 することは 必ずしも良いこと では ありません

記述したプログラムが わかりづらいかどうか は、人によって異なる ので、今回の記事で紹介した簡略化を 採用するかどうか は、自分で判断 して下さい。たとえば、list 内包表記がどうしても わかりづらい と思った方は、無理して list 内包表記を 使う必要はありません

ただし、以前の記事でも言及しましたが、他人が記述 したプログラムを 参考にする場合 は、今回の記事で紹介したような 簡略化の記述方法の知識 がどうしても 必要になります。今回の記事で紹介した簡略化の方法を 自分では使わない場合でも、そのような簡略化の方法が 存在するということ は、頭の片隅 にでも 覚えておいたほうが良い でしょう。具体的な方法を 暗記していなくても、そのような方法がある事を知っていれば、調べて理解できるようになる からです。

プログラムの 簡略化 によってプログラムが わかりづらくなる という デメリットがある 場合は、簡略化することによる メリット と、デメリットどちらが上回るかを考慮 した上で、実際に簡略化を行うかどうかの 判断を行う 必要がある。

x の値coord[0]y の値coord[1] なので、上記のプログラムは、下記のプログラムのように さらに簡略化 することもできますが、このプログラムを見て意味がすぐに分かる人はどれだけいるでしょうか?個人的 には ここまでの簡略化お勧めしません

def is_same(self, mark, coord, dx, dy):
    return sum([self.board[coord[0] + i * dx][coord[1] + i * dy] == mark
                for i in range(self.BOARD_SIZE)]) == self.BOARD_SIZE

Marubatsu.is_same = is_same
修正箇所
def is_same(self, mark, coord, dx, dy):
-   x, y = coord
-   return sum([self.board[x + i * dx][y + i * dy] == mark
+   return sum([self.board[coord[0] + i * dx][coord[1] + i * dy] == mark
                for i in range(self.BOARD_SIZE)]) == self.BOARD_SIZE

Marubatsu.is_same = is_same

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

is_same のアルゴリズム その 5 (+ 演算子の利用)

is_same を、前回の記事で紹介した、+ 演算子 を使うアルゴリズムで実装することができます。

具体的には、下記のプログラムのように、is_same調べるマスマーク を表す文字列を + 演算子で連結 した 文字列を計算 し、その文字列が 調べるマスの数 だけ mark並んだ文字列等しいかどうか判定 を行います。修正点は以下の通りです。

  • 3 行目:一直線(line)上のマスの マーク の文字列を 連結した文字列(text)を計算するための、line_text という変数を、空文字で初期化 する
  • 5 行目line_text に、(x, y) のマスの マーク文字列を連結 する
  • 9 行目line_text と、調べるマスの数 を表す self_BOARD_SIZE だけ mark が 連結 した 文字列を比較 し、等しい 場合は Trueそうでない 場合は False返す
 1  def is_same(self, mark, coord, dx, dy):
 2      x, y = coord
 3      line_text = ""
 4      for _ in range(self.BOARD_SIZE):
 5          line_text += self.board[x][y]
 6          x += dx
 7          y += dy
 8
 9      return line_text == mark * self.BOARD_SIZE
10
11  Marubatsu.is_same = is_same
行番号のないプログラム
def is_same(self, mark, coord, dx, dy):
    x, y = coord
    line_text = ""
    for _ in range(self.BOARD_SIZE):
        line_text += self.board[x][y]
        x += dx
        y += dy

    return line_text == mark * self.BOARD_SIZE

Marubatsu.is_same = is_same
修正箇所(その 2 と比較しています)
def is_same(self, mark, coord, dx, dy):
    x, y = coord
-   count = 0
+   line_text = ""
    for _ in range(self.BOARD_SIZE):
-       if self.board[x][y] == mark:
-           count += 1
+       line_text += self.board[x][y]
        x += dx
        y += dy

-   return count == self.BOARD_SIZE
+   return line_text == mark * self.BOARD_SIZE

Marubatsu.is_same = is_same

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

list と join を使った記述方法

以前の記事 で説明したように、文字列型 のデータ のみ要素 として持つ list の要素 を、join メソッド を使って、特定の文字列間を区切って連結 することができます。その際に、空文字 に対して join メソッド を記述することで、list の要素直接連結 した 文字列を計算 できます。

従って、先程のプログラムを、下記のように記述することができます。3、4 行目で list 内包表記 を使っていますが、分かりづらい と思った人は for 文で記述 しても 構いません

  • 3、4 行目:一直線(line)上のマスの マーク の文字列を 要素 として持つ listlist 内包表記で計算 し、text_list に代入す
  • 5 行目空文字 に対して、text_list実引数に記述 した join メソッド を呼び出すことで、text_list要素を連結 した 文字列を計算 して、line_text に代入する
1  def is_same(self, mark, coord, dx, dy):
2      x, y = coord
3      text_list = [self.board[x + i * dx][y + i * dy] 
4                   for i in range(self.BOARD_SIZE)]
5      line_text = "".join(text_list)
6      return line_text == mark * self.BOARD_SIZE
7
8  Marubatsu.is_same = is_same
行番号のないプログラム
def is_same(self, mark, coord, dx, dy):
    x, y = coord
    text_list = [self.board[x + i * dx][y + i * dy] 
                 for i in range(self.BOARD_SIZE)]
    line_text = "".join(text_list)
    return line_text == mark * self.BOARD_SIZE

Marubatsu.is_same = is_same
修正箇所
Marubatsu.is_same = is_same
def is_same(self, mark, coord, dx, dy):
    x, y = coord
-   line_text = ""
-   for _ in range(self.BOARD_SIZE):
-       line_text += self.board[x][y]
-       x += dx
-       y += dy
+   text_list = [self.board[x + i * dx][y + i * dy] 
+                for i in range(self.BOARD_SIZE)]
+   line_text = "".join(text_list)

    return line_text == mark * self.BOARD_SIZE

Marubatsu.is_same = is_same

下記のプログラムを実行することで、修正した judge のテストを行うことができます。実行結果は先程と同じなので省略します。

test_judge()

このアルゴリズムの利点と欠点

このアルゴリズムの 利点 は、条件式の記述 が、最後の line_text == mark * self.BOARD_SIZE のみ である点にあります。条件式 は、条件式の 計算結果 によって、処理の流れを変化させる ためのものなので、条件式を記述 すると、記述した分だけ プログラムの 処理の流れが複雑になる からです。

欠点 としては、アルゴリズムで行われる 処理が直観的ではない 点が挙げられます。一直線上に 同じマークが並んでいる ことを 判定 する際に、「全てのマスに同じマークが配置されているかどうかを調べる」、「マークが配置されているマスの数を数える」というアルゴリズムは、直観的に分かりやすい と思いますが、このアルゴリズムのように「マークを表す文字列を連結し、連結した文字列とマークを並べた文字列を比較する」という方法は 正攻法ではなくすこしひねった アルゴリズムなので、直観的に分かりづらい のではないかと思います。

本記事のまとめ

本記事では、judge メソッドの中の、〇 と × の勝利を判定を行う アルゴリズムを紹介 しました。

下記の表は、それぞれのアルゴリズムの 特長を簡単にまとめた ものです。

アルゴリズム 利点 欠点
is_winner
その 6
あらかじめデータを作成しない
その 5 より見た目が少しわかりやすい
判定をまとめて行えない
is_same その 2
(数える)
並んでいるマークの数を数える必要がある場合に利用できる その 1 より処理に時間がかかる
is_same その 3
(list と sum
その 4 への布石なので特になし 慣れないとわかりづらい
is_same その 4
(list 内包表記)
プログラムを短く記述できる 慣れないとさらにわかりづらい
is_same その 5
join
条件式が少ないので処理の流れが簡潔 アルゴリズムが直観的ではない
初心者が思いつくのが難しい

本記事では、紹介したアルゴリズムの中で、is_winnerその 6 を、is_same は(どれでもあまり変わらないような気がしますが)その 5 を採用することにします。

次回の記事では、すべてのマスが埋まっていることを判定する様々なアルゴリズムを紹介します。

本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

以下のリンクは、今回の記事で更新した marubatsu.py です。

次回の記事

更新履歴

更新日時 更新内容
2023/12/11 is_winner の仮引数の名前を winner から player に修正しました
0
0
2

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
0