0
0

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を一から作成する その51 〇×ゲームに勝つための条件

Last updated at Posted at 2024-02-04

目次と前回の記事

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

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

これまでに作成した AI

これまでに作成した AI の アルゴリズム は以下の通りです。今回の記事から、左の列を「関数名」から「ルール」に変更しました。

ルール アルゴリズム
ルール1 左上から順空いているマス を探し、最初に見つかったマス着手 する
ルール2 ランダム なマスに 着手 する
ルール3 真ん中 のマスに 優先的着手 する
既に 埋まっていた場合ランダム なマスに 着手 する
ルール4 真ん中 のマスの 優先的着手 する
既に 埋まっていた場合ランダム なマスに 着手 する
ルール5 勝てる場合勝つ
そうでない場合は ランダム なマスに 着手 する
ルール6 勝てる場合勝つ
そうでない場合は 相手の勝利阻止 する
そうでない場合は ランダム なマスに 着手 する
ルール6改 勝てる場合勝つ
そうでない場合は 相手勝利できる 着手を 行わない
そうでない場合は ランダム なマスに 着手 する
ルール7 真ん中 のマスに 優先的着手 する
そうでない場合は 勝てる場合勝つ
そうでない場合は 相手の勝利阻止 する
そうでない場合は ランダム なマスに 着手 する
ルール7改 真ん中 のマスに 優先的着手 する
そうでない場合は 勝てる場合勝つ
そうでない場合は 相手勝利できる 着手を 行わない
そうでない場合は ランダム なマスに 着手 する

基準となる ai2 との 対戦結果(単位は %)は以下の通りです。太字ai2 VS ai2 よりも 成績が良い 数値を表します。欠陥 の列は、アルゴリズム欠陥 があるため、ai2 との 対戦成績良くても強い とは 限らない ことを表します。欠陥の詳細については、関数名のリンク先の説明を見て下さい。

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分 欠陥
ai1
ai1s
78.1 17.5 4.4 44.7 51.6 3.8 61.4 34.5 4.1 あり
ai2
ai2s
58.7 28.8 12.6 29.1 58.6 12.3 43.9 43.7 12.5
ai3
ai3s
69.3 19.2 11.5 38.9 47.6 13.5 54.1 33.4 12.5
ai4
ai4s
83.0 9.5 7.4 57.2 33.0 9.7 70.1 21.3 8.6 あり
ai5
ai5s
81.2 12.3 6.5 51.8 39.8 8.4 66.5 26.0 7.4
ai6 88.9 2.2 8.9 70.3 6.2 23.5 79.6 4.2 16.2
ai6s 88.6 1.9 9.5 69.4 9.1 21.5 79.0 5.5 15.5
ai7
ai7s
95.8 0.2 4.0 82.3 2.4 15.3 89.0 1.3 9.7

〇×ゲームに勝つ方法の考察の必要性

以前の記事で説明したように、ルールベース の AI は、「解決すべき問題 に対する、人間の知識ルール化 し、その ルールに基づいて AI が 判断を行う」という AI です。

従って、ルールベース強い AI作成 するためには、〇×ゲーム に関する 深い知識 が必要になります。ルール 5 の「勝てる場合に勝つ」と、ルール 6 の「相手の勝利を阻止する」は、〇×ゲームを少し遊べば 誰でも思いつく ルールだと思いますが、それ以上に強い AI を作成するためには、より深い 〇×ゲームの 知識が必要 になります。

今回の記事から、〇×ゲーム勝つ方法考察 し、その 考察に基づいた AI を 作成 します。〇×ゲームがあまり強くない人は、〇×ゲームに 勝つ方法 について考えてみて下さい。また、〇×ゲームがある程度強い人は、その 方法ルール化 して下さい。

勝利するために必要な条件

ゲームに 強くなるため必要最低限考察 として、以下のようなものが挙げらるでしょう。そこで、まず、〇×ゲームで 勝つため必要な条件 について 考察 することにします。どのような条件が必要であるかについて少し考えてみて下さい。

  • ゲームに 勝つため条件
  • ゲームに 負けないため条件

〇×ゲームは、以下性質 を持つゲームです。

  1. 交互空いているマス自分のマーク配置 する
  2. 一度 マークが配置 された マス は、ゲーム終了まで 変化しない
  3. 8 種類 ある 直線上の 3 マスいずれか に、自分のマーク3 つ並ぶ勝利 する

上記の 性質 3 から、自分勝利できる 局面は、いずれか直線上の 3 マス が、以下の状態 になっている 必要がある ことがわかります。

  • 自分のマーク2 つ配置 されている
  • 空いているマス1 つある

直線上の 3 つのマス配置の状態簡潔に表現 するために、以後は、「直線上の 3 マスに、自分のマークx 個相手のマークy 個空いている マスが z 個 ある」ことを、「自 x 敵 y 空 z」 のように 表記 することにしま

例えば、上記の場合は、「自 2 敵 0 空 1」と 表記 します。

上記の考察から、以下の事が分かります。

〇×ゲーム勝つため には、自分の手番着手 を行った 結果、「自 2 敵 0 空 1」が 1 つ以上 存在する 局面になる着手を行う 必要 がある。

〇×ゲームは、直線上マスの数 が常に 3 なので、「自 x 敵 y 空 z」の xyz には、$x + y + z = 3$ という関係があります。これは、xyz のうち、2 つの値決まる と、残り の 1 つの 自動的に決まる ということを表します。従って、「自 x 敵 y 空 z」は、その中の いずれか 1 つを削除 して「自 x 敵 y」のように 記述 しても かまいません が、本記事では わかりやすさ を重視して 3 つすべて記述 することにします。

余談ですが、複数変数 のうち、自由(独立に)に変更 できる 変数 の事を 自由度 と呼びます。「自 x 敵 y 空 z」の場合の自由度は 2 です。

また、count_marks の処理を、いずれかの 2 つの数 だけを 数える ように 修正 することも 可能 ですが、そのように修正するとかえって プログラムが複雑 になるので、修正しないほうが良い でしょう。

ルール 8 の定義

もちろん、上記の条件 を満たすような 着手 を行っても、次の相手の手番で 邪魔をされる場合がある ので 自分の勝ち確定 するわけでは ありません が、少なくとも 上記の 条件を満たす着手行わなければ自分が勝利 することは あり得ません

そこで、上記 を満たす 着手優先的に行う という 条件ルール 7 改 に加えた 下記の ルール 8 を考えることにします。なお、上記の条件満たしても、その次の 相手の手番相手が勝利 しては 意味がない ので、上記の条件 は、「相手勝利できる 着手を 行わない」という 条件より優先順位を低く する 必要 があります。

  • 真ん中 のマスに 優先的着手 する
  • そうでない場合は 勝てる場合勝つ
  • そうでない場合は 相手勝利できる 着手を 行わない
  • そうでない場合は、自分の手番勝利できる ように、「自 2 敵 0 空 1」が 1 つ以上 存在する 局面になる着手を行う
  • そうでない場合は ランダム なマスに 着手 する

ルール 8評価値 をどのように 設定 すればよいかについて少し考えてみて下さい。

評価値の設定

下記は、ルール 7 改評価値設定 です。

優先順位 局面の状況 評価値
1 真ん中のマスに着手している 2
2 自分が勝利している 1
4 相手が勝利できない 0
3 相手が勝利できる -1

上記の表に、ルール 8 の『「自 2 敵 0 空 1」が 1 つ以上 存在する』 という 新しい条件追加 した状態の 評価値を設定 する必要があります。

下記の表は、ルール 8条件の優先順位 に注意しながら 評価値設定 した例です。上 2 つ評価値 は、新しい条件の追加に合わせて 修正 しました。

優先順位 局面の状況 評価値
1 真ん中のマスに着手している 3
2 自分が勝利している 2
4 「自 2 敵 0 空 1」が 1 つ以上存在する 1
5 「自 2 敵 0 空 1」が存在しない 0
3 相手が勝利できる -1

ai8s の定義

局面に 「自 2 敵 0 空 1」が 1 つ以上存在するか どうかは、前回の記事で定義した count_marks メソッドで 計算 できるので、ai8s は、下記のように定義できます。

  • 7、8 行目真ん中 のマスに着手した時の 評価値3修正 する
  • 11、12 行目自分が勝利 している場合の 評価値2修正 する
  • 23、24、28、29、34、35、40、41 行目相手勝利できない 局面で、自分の手番自分勝利できる かどうかを、「自 2 敵 0 空 1」であるかどうかで 判定 し、その場合は 評価値 として 1返すmb相手の手番 の局面であり、自分の手番 は、mb.last_turn代入 されている点に 注意 すること
 1  from ai import ai_by_score
 2  from marubatsu import Marubatsu
 3
 4  def ai8s(mb, debug=False):
 5      def eval_func(mb):
 6          # 真ん中のマスに着手している場合は、評価値として 3 を返す
 7          if mb.last_move == (1, 1):
 8              return 3
 9   
10          # 自分が勝利している場合は、評価値として 2 を返す
11          if mb.status == mb.last_turn:
12              return 2
13
14          # 相手の手番で相手が勝利できる場合は評価値として -1 を返す
15          # そうでなく、「自 2 敵 0 空 1」がある場合は評価値として 1 を返す
16          # 横方向と縦方向の判定
17          for i in range(mb.BOARD_SIZE):
18              count = mb.count_marks(coord=[0, i], dx=1, dy=0)
19              # 「自 0 敵 2 空 1」の場合は相手が勝利できる
20              if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
21                  return -1
22              # 「自 2 敵 0 空 1」の場合は次の自分の手番で自分が勝利できる
23              elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
24                  return 1
25              count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
26              if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
27                  return -1
28              elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
29                  return 1
30          # 左上から右下方向の判定
31          count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
32          if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
33              return -1
34          elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
35              return 1
36          # 右上から左下方向の判定
37          count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
38          if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
39              return -1
40          elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
41              return 1
42
43          # それ以外の場合は評価値として 0 を返す
44          return 0
45
46      return ai_by_score(mb, eval_func, debug=debug)  
行番号のないプログラム
from ai import ai_by_score
from marubatsu import Marubatsu

def ai8s(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 3 を返す
        if mb.last_move == (1, 1):
            return 3
    
        # 自分が勝利している場合は、評価値として 2 を返す
        if mb.status == mb.last_turn:
            return 2

        # 相手の手番で相手が勝利できる場合は評価値として -1 を返す
        # そうでなく、「自 2 敵 0 空 1」がある場合は評価値として 1 を返す
        # 横方向と縦方向の判定
        for i in range(mb.BOARD_SIZE):
            count = mb.count_marks(coord=[0, i], dx=1, dy=0)
            # 「自 0 敵 2 空 1」の場合は相手が勝利できる
            if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return -1
            # 「自 2 敵 0 空 1」の場合は次の自分の手番で自分が勝利できる
            elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return 1
            count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
            if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return -1
            elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return 1
        # 左上から右下方向の判定
        count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
        elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return 1
        # 右上から左下方向の判定
        count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
        elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return 1

        # それ以外の場合は評価値として 0 を返す
        return 0

    return ai_by_score(mb, eval_func, debug=debug) 
修正箇所
from ai import ai_by_score
from marubatsu import Marubatsu

-def ai7s(mb, debug=False):
+def ai8s(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 3 を返す
        if mb.last_move == (1, 1):
-           return 2
+           return 3
    
        # 自分が勝利している場合は、評価値として 2 を返す
        if mb.status == mb.last_turn:
-           return 1
+           return 2

        # 相手の手番で相手が勝利できる場合は評価値として -1 を返す
        # そうでなく、「自 2 敵 0 空 1」がある場合は評価値として 1 を返す
        # 横方向と縦方向の判定
        for i in range(mb.BOARD_SIZE):
            count = mb.count_marks(coord=[0, i], dx=1, dy=0)
            # 「自 0 敵 2 空 1」の場合は相手が勝利できる
            if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return -1
            # 「自 2 敵 0 空 1」の場合は次の自分の手番で自分が勝利できる
+           elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
+               return 1
            count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
            if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return -1
+           elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
+               return 1
        # 左上から右下方向の判定
        count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
+       elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
+           return 1
        # 右上から左下方向の判定
        count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
+       elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
+           return 1

        # それ以外の場合は評価値として 0 を返す
        return 0

    return ai_by_score(mb, eval_func, debug=debug) 

ai2ai7s との対戦

基準となる ai2 と、これまで に作成した 最強の AI である ai7s と対戦することにします。まず、下記のプログラムで ai2 と対戦します。

from ai import ai_match, ai2, ai7s

ai_match(ai=[ai8s, ai2])

実行結果(実行結果はランダムなので下記とは異なる場合があります)

ai8s VS ai2
count     win    lose    draw
o        9726     148     126
x        8608     854     538
total   18334    1002     664

ratio     win    lose    draw
o       97.3%    1.5%    1.3%
x       86.1%    8.5%    5.4%
total   91.7%    5.0%    3.3%

下記は、ai7 VS ai2ai8s VS ai2 の対戦結果の表です。ai2 に対してai8s のほうが、ai7 より 勝率が高くなる ことが確認できますが、よく見ると、敗率も高く なっています。ルール 8条件 4追加敗率が高くなる のは 何かが変 です。

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分 欠陥
ai7 95.8 0.2 4.0 82.3 2.4 15.3 89.0 1.3 9.7
ai8s 97.3 1.5 1.3 86.1 8.5 5.4 91.7 5.0 3.3

次に、ai7s対戦 を行います。実行結果から、強くしたはずai8sai7 対して 大きく負け越している 点が です。これは、ルール 8欠陥がある か、ai8sルール 8正しく実装できていない かの どちらか原因 です。ルール 8見直しても 、特に 間違いなさそう なので、ai8sバグがある 可能性が高く、実際に ai8s には バグがあります。それが何であるかについて少し考えてみて下さい。

ai_match(ai=[ai8s, ai7s])

実行結果(実行結果はランダムなので下記とは異なる場合があります)

ai8s VS ai7s
count     win    lose    draw
o        2563    3392    4045
x          92    7373    2535
total    2655   10765    6580

ratio     win    lose    draw
o       25.6%   33.9%   40.5%
x        0.9%   73.7%   25.4%
total   13.3%   53.8%   32.9%

ai8s のバグの原因

下記は、ai8s一部を抜粋 したものです。5 ~ 9 行目 のプログラムに バグがある ので、バグがどこにあるかが分からなかった方はその部分を探してみて下さい。

1        # 横方向と縦方向の判定
2        for i in range(mb.BOARD_SIZE):
3            count = mb.count_marks(coord=[0, i], dx=1, dy=0)
4            # 「自 0 敵 2 空 1」の場合は相手が勝利できる
5            if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
6                return -1
7            # 「自 2 敵 0 空 1」の場合は次の自分の手番で自分が勝利できる
8            elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
9                return 1

バグが発生 する 具体的な局面 を例に 説明 します。例えば 下図左〇 の手番 の局面で、ルール 8着手を選択 する場合の事を考えてみて下さい。その場合は、まず、最初の合法手 として、下図右赤い 〇着手 した局面に対して、上記の処理 を行って 評価値を計算 することになります。

上図右局面 に対して上記のプログラムを実行すると、下記処理 が行われます。

  • 2 行目i0代入 されて for 文ブロックが実行 される
  • 3 行目:上図右の 上の行 の「〇〇 」の マークの数数え結果 の「〇 2 × 0 空 1」を表すデータ count代入 する。元の局面〇 の手番 なので、これは「自 2 敵 0 空 1」を表すデータである
  • 5 行目count の内容が、勝利できる、「自 0 敵 2 空 1ではない ので、条件式False になる
  • 8、9 行目count の内容が、自分の手番自分勝利できる、「自 2 敵 0 空 1」なので、条件式True になり、評価値 として 1 を返り値として 返す

上図右局面 は、相手勝利できる ので、評価値 として -1返す必要 がありますが、上記の手順で処理が行われるため、1評価値 として 返り ます。従って、上図右 の局面になるような 着手行うべきではない のに、ai8s はそのマスに 着手 してしまう 可能性生じます。これが、ai8sai7 に負け越す原因 です。

下記のプログラムは、上図左 の局面に対して、ai8sデバッグ表示 を行いながら 着手を選択 するプログラムです。実行結果 から、実際に 上図右 の局面の 評価値 として 1計算 されていることが分かります。

mb = Marubatsu()
mb.move(0, 0)
mb.move(0, 1)
mb.move(0, 2)
mb.move(1, 1)
print(mb)

ai8s(mb, debug=True)

実行結果

Turn o
o..
xX.
o..

Start ai_by_score
Turn o
o..
xX.
o..

legal_moves [(1, 0), (2, 0), (2, 1), (1, 2), (2, 2)]
====================
move (1, 0)
Turn x
oO.
xx.
o..

score 1 best score -inf
UPDATE
  best score 1
  best moves [(1, 0)]
====================
move (2, 0)
Turn x
o.O
xx.
o..

score 1 best score 1
APPEND
  best moves [(1, 0), (2, 0)]
====================
move (2, 1)
Turn x
o..
xxO
o..

score 0 best score 1
====================
move (1, 2)
Turn x
o..
xx.
oO.

score -1 best score 1
====================
move (2, 2)
Turn x
o..
xx.
o.O

score -1 best score 1
====================
Finished
best score 1
best moves [(1, 0), (2, 0)]

また、残りの合法手 に着手した局面の 評価値 は、下記 のように 計算 されます。

  • (2, 0)(1, 0)同様の手順 で計算が行われ、1計算 される
  • (2, 1):「自 0 敵 2 空 1」も「自 2 敵 0 空 1」も 存在しない ので 0計算 される
  • (1, 2)(2, 2):「自 0 敵 2 空 1のみが存在 するので、-1計算 される

結果 として、最善手一覧(1, 0)(2, 0) のマスが 計算 され、その いずれか のマスに 着手行われます が、その次相手の手番 で 相手が (2, 1)着手 して 勝利 することが できてしまいます。本当は、(2, 1)着手 することで 相手の勝利阻止できていた ので、ai8s間違った 着手を 選択する ことが 確認 できました。

このようなバグが発生した理由について少し考えてみてください。

ai8s の修正方法 その 1

このバグは、ai8s が、下記ルール 8 の表の、優先順位34条件繰り返し処理の中if 文まとめて判定 しているため、繰り返しの途中優先順位4 の条件が、3 の条件 よりも優先 されてしまう 場合がある事原因 です。従って、優先順位3 の条件が、4 の条件 より必ず優先される ようにすることで バグを修正 することができます。その方法について少し考えてみて下さい。

優先順位 局面の状況 評価値
1 真ん中のマスに着手している 3
2 自分が勝利している 2
4 「自 2 敵 0 空 1」が 1 つ以上存在する 1
5 「自 2 敵 0 空 1」が存在しない 0
3 相手が勝利できる -1

記述は大変 ですが、最も単純 な方法は、下記のプログラムのように、優先順位3 の条件の 判定終了してから優先順位4 の条件の 判定を行う というものです。

  • 13 ~ 27 行目相手勝利できるか どうか だけ判定 するように 修正 する
  • 31 ~ 45 行目自分の手番自分勝利できるか判定 する

比べて見ればわかると思いますが、13 ~ 27 行目 と、31 ~ 45 行目 のプログラムは、条件式 と、return 文返り値除けば全く同じ処理 を行います。

 1  def ai8s(mb, debug=False):
 2      def eval_func(mb):
 3          # 真ん中のマスに着手している場合は、評価値として 3 を返す
 4          if mb.last_move == (1, 1):
 5              return 3
 6    
 7          # 自分が勝利している場合は、評価値として 2 を返す
 8          if mb.status == mb.last_turn:
 9              return 2
10
11          # 相手の手番で相手が勝利できる場合は評価値として -1 を返す
12          # 横方向と縦方向の判定
13          for i in range(mb.BOARD_SIZE):
14              count = mb.count_marks(coord=[0, i], dx=1, dy=0)
15              if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
16                  return -1
17              count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
18              if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
19                  return -1
20          # 左上から右下方向の判定
21          count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
22          if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
23              return -1
24          # 右上から左下方向の判定
25          count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
26          if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
27              return -1
28
29          # そうでなく、「自 2 敵 0 空 1」がある場合は評価値として 1 を返す
30          # 横方向と縦方向の判定
31          for i in range(mb.BOARD_SIZE):
32              count = mb.count_marks(coord=[0, i], dx=1, dy=0)
33              if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
34                  return 1
35              count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
36              if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
37                  return 1
38          # 左上から右下方向の判定
39          count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
40          if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
41              return 1
42          # 右上から左下方向の判定
43          count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
44          if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
45              return 1            
46
47          # それ以外の場合は評価値として 0 を返す
48          return 0
49
50      return ai_by_score(mb, eval_func, debug=debug) 
行番号のないプログラム
def ai8s(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 3 を返す
        if mb.last_move == (1, 1):
            return 3
    
        # 自分が勝利している場合は、評価値として 2 を返す
        if mb.status == mb.last_turn:
            return 2

        # 相手の手番で相手が勝利できる場合は評価値として -1 を返す
        # 横方向と縦方向の判定
        for i in range(mb.BOARD_SIZE):
            count = mb.count_marks(coord=[0, i], dx=1, dy=0)
            if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return -1
            count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
            if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return -1
        # 左上から右下方向の判定
        count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
        # 右上から左下方向の判定
        count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1

        # そうでなく、「自 2 敵 0 空 1」がある場合は評価値として 1 を返す
        # 横方向と縦方向の判定
        for i in range(mb.BOARD_SIZE):
            count = mb.count_marks(coord=[0, i], dx=1, dy=0)
            if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return 1
            count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
            if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return 1
        # 左上から右下方向の判定
        count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
        if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return 1
        # 右上から左下方向の判定
        count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
        if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return 1            

        # それ以外の場合は評価値として 0 を返す
        return 0

    return ai_by_score(mb, eval_func, debug=debug) 
修正箇所
def ai8s(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 3 を返す
        if mb.last_move == (1, 1):
            return 3
    
        # 自分が勝利している場合は、評価値として 2 を返す
        if mb.status == mb.last_turn:
            return 2

        # 相手の手番で相手が勝利できる場合は評価値として -1 を返す
        # 横方向と縦方向の判定
        for i in range(mb.BOARD_SIZE):
            count = mb.count_marks(coord=[0, i], dx=1, dy=0)
            # 「自 0 敵 2 空 1」の場合は相手が勝利できる
            if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return -1
            # 「自 2 敵 0 空 1」の場合は次の自分の手番で自分が勝利できる
-           elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
-               return 1
            count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
            if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
                return -1
-           elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
-               return 1
        # 左上から右下方向の判定
        count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
-       elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
-           return 1
        # 右上から左下方向の判定
        count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
        if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:
            return -1
-       elif count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
-           return 1

        # そうでなく、「自 2 敵 0 空 1」がある場合は評価値として 1 を返す
        # 横方向と縦方向の判定
+       for i in range(mb.BOARD_SIZE):
+           count = mb.count_marks(coord=[0, i], dx=1, dy=0)
+           if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
+               return 1
+           count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
+           if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
+               return 1
+       # 左上から右下方向の判定
+       count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
+       if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
+           return 1
+       # 右上から左下方向の判定
+        count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
+       if count[mb.last_turn] == 2 and count[Marubatsu.EMPTY] == 1:
+           return 1            

        # それ以外の場合は評価値として 0 を返す
        return 0

    return ai_by_score(mb, eval_func, debug=debug) 

修正の確認

修正したプログラムが 正しく動作 するかを 確認 するために、再び ai2ai7s との対戦を行います。まず、基準となる ai2対戦 します。

ai_match(ai=[ai8s, ai2])

実行結果(実行結果はランダムなので下記とは異なる場合があります)

ai8s VS ai2
count     win    lose    draw
o        9824      14     162
x        8938     249     813
total   18762     263     975

ratio     win    lose    draw
o       98.2%    0.1%    1.6%
x       89.4%    2.5%    8.1%
total   93.8%    1.3%    4.9%

下記は、ai7 VS ai2ai8s VS ai2 の対戦結果の表です。今度ai2 に対して、ai8s のほうが、ai7 より 勝率が高く なり、敗率ほぼ変わらない ので、正しく修正 された 可能性が高い ことが 確認 できます。

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分 欠陥
ai7 95.8 0.2 4.0 82.3 2.4 15.3 89.0 1.3 9.7
ai8s 98.2 0.1 1.6 89.4 2.5 8.1 93.8 1.3 4.9

続いて、ai7s との 対戦 を行います。実行結果 から、通算成績 では、ai8s勝率 の方が 敗率より高い ので、ai8sai7s 対して 強い ことが 確認 できました。

ai_match(ai=[ai8s, ai7s])

実行結果(実行結果はランダムなので下記とは異なる場合があります)

ai8s VS ai7s
count     win    lose    draw
o        3343     393    6264
x         388    2884    6728
total    3731    3277   12992

ratio     win    lose    draw
o       33.4%    3.9%   62.6%
x        3.9%   28.8%   67.3%
total   18.7%   16.4%   65.0%

ai8s の修正方法 その 2

先程ai8s では、以下 のような 手順処理 を行いました。

  • 8 種類ある 直線上の 3 マス に「自 0 敵 2 空 1」が 存在する 場合は -1 を返す
  • 8 種類ある 直線上の 3 マス に「自 2 敵 0 空 1」が 存在する 場合は 1 を返す

上記の 2 つの処理 は、同じようなかなり長い プログラムを 記述 する必要があるので 大変 です。そこで、上記の 2 つの処理1 つにまとめる ことにします。また、1 つにまとめる ことで、処理時間半分になる という 利点 が得られます。どのようにすればまとめることができるかについて、少し考えてみて下さい。

マークの数のパターンを列挙する関数の定義

説明簡潔にする ために、以後は、「自 2 敵 0 空 1」のような、直線上に配置 された マーク配置状況 の事を、「マークのパターン」と 表記 します。

ルール 8評価値計算 する際に 必要な処理 は、「自 0 敵 2 空 1」と「自 2 敵 0 空 1」という 2 種類マークのパターン存在するか調べる という処理です。

先ほどのプログラムでは、それらが 存在するか調べる ために、8 種類 ある 直線上の 3 マスマークのパターン を調べるという処理を、2 回に分けて 行いました。それに対して、下記のアルゴリズムでは、8 種類 ある 直線上の 3 マスマークのパターン一度調べるだけ同様の処理 を行うことができます。

  1. 8 種類 ある 直線上の 3 マスマークのパターン を計算し、結果登録 する
  2. 登録 した データの中 に、「自 0 敵 2 空 1」が あれば -1返す
  3. 登録 した データの中 に、「自 2 敵 0 空 1」が あれば 1返す

別の言葉で説明すると、特定マークのパターン存在するか どうかを 調べる際 に、マークのパターンごと8 種類 ある 直線上の 3 マス調べ直すではなく調べた結果登録 しておくことで、一度調べるだけ で済むということです。

そこで、Marubatsu クラスに 上記手順 1処理 を行う、以下のような メソッド定義 することで、Marubatsu クラスの インスタンス に対して、局面に存在 する マークのパターン一覧計算できる ようにします。

名前マーク(mark)の パターン(pattern)を 列挙(enumerate)するので、enum_markpats という名前にします
処理:インスタンスの 局面マークのパターン列挙 する
入力:なし
出力マークのパターン列挙 した データ返り値 として 返す

マークのパターン列挙 する データ型 として、真っ先に思いつくのは list ではないかと思います。そこで、今回の記事では、list を利用 して enum_markpat を下記のプログラムのように定義することにします。行っている処理は、ai8s の中の 処理あまり変わらない ので、それほど難しくはないと思います。

  • 2 行目マークのパターン列挙 した データ代入 するローカル変数 markpats空の list初期化 する
  • 5 ~ 15 行目8 種類 ある 直線上の 3 マスマークのパターン を、これまでと同様の方法で 計算 する
  • 7、9、12、15 行目:計算したそれぞれの マークのパターン を、marks_pats要素 として 追加 する
  • 17 行目markpats返り値 として 返す
 1  def enum_markpats(self):
 2      markpats = []    
 3
 4      # 横方向と縦方向の判定
 5      for i in range(self.BOARD_SIZE):
 6          count = self.count_marks(coord=[0, i], dx=1, dy=0)
 7          markpats.append(count)
 8          count = self.count_marks(coord=[i, 0], dx=0, dy=1)
 9          markpats.append(count)
10      # 左上から右下方向の判定
11      count = self.count_marks(coord=[0, 0], dx=1, dy=1)
12      markpats.append(count)
13      # 右上から左下方向の判定
14      count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
15      markpats.append(count)
16
17      return markpats
18
19  Marubatsu.enum_markpats = enum_markpats
行番号のないプログラム
def enum_markpats(self):
    markpats = []    
 
    # 横方向と縦方向の判定
    for i in range(self.BOARD_SIZE):
        count = self.count_marks(coord=[0, i], dx=1, dy=0)
        markpats.append(count)
        count = self.count_marks(coord=[i, 0], dx=0, dy=1)
        markpats.append(count)
    # 左上から右下方向の判定
    count = self.count_marks(coord=[0, 0], dx=1, dy=1)
    markpats.append(count)
    # 右上から左下方向の判定
    count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
    markpats.append(count)

    return markpats

Marubatsu.enum_markpats = enum_markpats

ai8s の場合と異なり、計算した マークのパターン は、markpats要素に追加するだけ なので、上記のプログラムを下記のように 簡潔に記述 することもできます。本記事では、下記の記述を採用しますが、好きな方を採用して構いません。

def enum_markpats(self):
    markpats = []    
 
    # 横方向と縦方向の判定
    for i in range(self.BOARD_SIZE):
        markpats.append(self.count_marks(coord=[0, i], dx=1, dy=0))
        markpats.append(self.count_marks(coord=[i, 0], dx=0, dy=1))
    # 左上から右下方向の判定
    markpats.append(self.count_marks(coord=[0, 0], dx=1, dy=1))
    # 右上から左下方向の判定
    markpats.append(self.count_marks(coord=[2, 0], dx=-1, dy=1))

    return markpats

Marubatsu.enum_markpats = enum_markpats
修正箇所
def enum_markpats(self):
    markpats = []    

    # 横方向と縦方向の判定
    for i in range(self.BOARD_SIZE):
-       count = self.count_marks(coord=[0, i], dx=1, dy=0)
-       markpats.append(count)
+       markpats.append(self.count_marks(coord=[0, i], dx=1, dy=0))
-       count = self.count_marks(coord=[i, 0], dx=0, dy=1)
-       markpats.append(count)
+       markpats.append(self.count_marks(coord=[i, 0], dx=0, dy=1))
    # 左上から右下方向の判定
-   count = self.count_marks(coord=[0, 0], dx=1, dy=1)
-   markpats.append(count)
+   markpats.append(self.count_marks(coord=[0, 0], dx=1, dy=1))
    # 右上から左下方向の判定
-   count = self.count_marks(coord=[2, 0], dx=-1, dy=1)
-   markpats.append(count)
+   markpats.append(self.count_marks(coord=[2, 0], dx=-1, dy=1))
    
    return markpats

Marubatsu.enum_markpats = enum_markpats

enum_markpats の実装の確認

下記のプログラムは、いくつかの局面 に対して、enum_markpats メソッドを呼び出し て 計算 した、マークのパターン列挙 した データを表示 しています。なお、分かりやすく表示 するために、表示 には以前の記事で紹介した pprint を利用します。

実行結果 から、それぞれの局面 に対して、enum_markpats が、8 種類 ある 直線上の 3 マスマークのパターン要素 とする list を返す ことが 確認 できます。

from pprint import pprint
mb = Marubatsu()

print(mb)
pprint(mb.enum_markpats())

mb.move(1, 1)
print(mb)
pprint(mb.enum_markpats())

mb.move(0, 0)
print(mb)
pprint(mb.enum_markpats())

mb.move(1, 0)
print(mb)
pprint(mb.enum_markpats())

実行結果

Turn o
...
...
...

[defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3})]
Turn x
...
.O.
...

[defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1})]
Turn o
X..
.o.
...

[defaultdict(<class 'int'>, {'x': 1, '.': 2}),
 defaultdict(<class 'int'>, {'x': 1, '.': 2}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'x': 1, 'o': 1, '.': 1}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1})]
Turn x
xO.
.o.
...

[defaultdict(<class 'int'>, {'x': 1, 'o': 1, '.': 1}),
 defaultdict(<class 'int'>, {'x': 1, '.': 2}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1}),
 defaultdict(<class 'int'>, {'o': 2, '.': 1}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'x': 1, 'o': 1, '.': 1}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1})]

dict と defaultdict の比較

list など の、シーケンス型要素の中特定の値 の要素が 存在するか どうかは、in 演算子 を使って 判定 することが できます。また、in 演算子 は、== 演算子 を使って、同じ値であるか どうかを 判定 します。enum_markpats は、defaultdict要素 とする list を返す ので、defaultdict に対する == 演算子処理 について説明します。

dict 動詞の比較

まず、dict どうし== 演算子比較 した場合の 処理 について説明します。

dict どうし== 演算子比較 した場合は、同一キーキーの値組み合わせ を持つ場合のみ 計算結果True になります。以下にいくつかの 具体例 を示します。

  • ab同一キーとキーの値組み合わせ を持つので、True になる
  • ac異なるキーとキーの値組み合わせ を持つので、False になる
  • ad は、一部の キーとキーの値 が異なるので False になる
  • ae のように、両方に存在 する キーとキーの値組み合わせ が同じだが、一部の キー片方dict にしか存在しない 場合は False になる
  • af のように、キー記述の順番異なっていても同一キーとキーの値組み合わせ を持つ場合は True になる
a = {"x": 1, "y": 2}
b = {"x": 1, "y": 2}
c = {"z": 1}
d = {"x": 1, "y": 3}
e = {"x": 1}
f = {"y": 2, "x": 1}

print(a == b)
print(a == c)
print(a == d)
print(a == e)
print(a == f)

実行結果

True
False
False
False
True

defaultdict どうしの比較

defaultdict どうし比較 は、dict どうし比較同じ なので説明は省略します。

dict と defaultdict の比較

dictdefaultdict比較dict どうし比較同様 で、下記のプログラムのように、同一キーとキーの値組み合わせ を持つ場合は True になります。

from collections import defaultdict

a = {"x": 1, "y": 2}
b = defaultdict(int)
b["x"] = 1
b["y"] = 2
print(a == b)

実行結果

True

従って、enum_markpats要素の中 に、「自 2 敵 0 空 1」が存在するかどうかは、下記のプログラムのように、「自 2 敵 0 空 1」を表す {Marubatsu.CIRCLE: 2, Marubatsu.CROSS: 0, Marubatsu.EMPTY: 1 }存在するか どうかで 判定できるはず考える人が多い かもしれません、実行 すると False表示 されてしまいます。

実行結果 に表示される markpats要素の中 には、確かに「自 2 敵 0 空 1」を表す defaultdict(<class 'int'>, {'o': 2, '.': 1})表示されている のに、False になる のは 理由があります。その理由について説明します。

print(mb)
markpats = mb.enum_markpats()
pprint(markpats)
print({Marubatsu.CIRCLE: 2, Marubatsu.CROSS: 0, Marubatsu.EMPTY: 1} in markpats)

実行結果

Turn x
xO.
.o.
...

[defaultdict(<class 'int'>, {'x': 1, 'o': 1, '.': 1}),
 defaultdict(<class 'int'>, {'x': 1, '.': 2}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1}),
 defaultdict(<class 'int'>, {'o': 2, '.': 1}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'x': 1, 'o': 1, '.': 1}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1})]
False

dict と defaultdict の比較の際の注意点

勘違いしやすい 点として、下記の場合 は、True にならない 点に 注意が必要 です。

  • dict に、比較する defaultdict規定値代入 されている キーが存在 する
  • 比較する defaultdict は、その キー一度も参照していない

具体例を挙げて説明します。下記のプログラムは、a"x" というキーの値に 1 を、"y" というキーの値に b の規定値 である 0代入 した dict を代入します。

b規定値0defaultdict には、"x" というキーの値に 1代入 します。

  • 5、6 行目ab表示 すると、a では "x""y"キーとキーの値表示 されるが、b では、"y" という キー一度も参照していない ので、"x"キーとキーの値 のみ 表示 されまる
  • 7 行目:上記のように、ab同じキーとキーの値組み合わせ持たない ので、a == bFalse になる
  • 9、11 行目a["y"]b["y"] を比較すると、b["y"] には、既定値 である 0自動的に代入 されるので、True になる。そのことは、11 行目確認 できる
  • 12 行目b には "y" という キーが存在するようになった ので、True になる
 1  a = {"x": 1, "y": 0}
 2  b = defaultdict(int)
 3  b["x"] = 1
 4
 5  print("a", a)
 6  print("b", b)
 7  print("a == b", a == b)
 8
 9  print('a["y"] == b["y"]', a ["y"] == b["y"])
10  print("a", a)
11  print("b", b)
12  print("a == b", a == b)
行番号のないプログラム
a = {"x": 1, "y": 0}
b = defaultdict(int)
b["x"] = 1

print("a:", a)
print("b:", b)
print("a == b:", a == b)

print('a["y"] == b["y"]', a ["y"] == b["y"])
print("a:", a)
print("b:", b)
print("a == b:", a == b)

実行結果

a: {'x': 1, 'y': 0}
b: defaultdict(<class 'int'>, {'x': 1})
a == b: False
a["y"] == b["y"] True
a: {'x': 1, 'y': 0}
b: defaultdict(<class 'int'>, {'x': 1, 'y': 0})
a == b: True

先程、{Marubatsu.CIRCLE: 2, Marubatsu.CROSS: 0, Marubatsu.EMPTY: 1 } in markpatsFalse になったのは、markpatsdefaultdict(<class 'int'>, {'o': 2, '.': 1}) という 要素Marubatsu.CROSS という キーが存在しない からです。

従って、enum_markpats要素の中 に、「自 2 敵 0 空 1」が存在するかどうかは、下記のプログラムのように、{Marubatsu.CIRCLE: 2, Marubatsu.EMPTY: 1 }存在するか どうかで 判定 する 必要 があり、今度は 実行結果True が表示 されます。

print(mb)
markpats = mb.enum_markpats()
print({ Marubatsu.CIRCLE: 2, Marubatsu.EMPTY: 1 } in markpats)

実行結果

Turn x
xO.
.o.
...

[defaultdict(<class 'int'>, {'x': 1, 'o': 1, '.': 1}),
 defaultdict(<class 'int'>, {'x': 1, '.': 2}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1}),
 defaultdict(<class 'int'>, {'o': 2, '.': 1}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'.': 3}),
 defaultdict(<class 'int'>, {'x': 1, 'o': 1, '.': 1}),
 defaultdict(<class 'int'>, {'.': 2, 'o': 1})]
True

dictdefaultdict を、== 演算子比較 することが できる

ただし、dictキーの値defaultdict規定値 であっても、defaultdictそのキー一度も参照されていない 場合は、等しいみなされい

enum_markpats を利用した ai8s の定義

enum_markpats メソッドを 利用 することで、ルール 8 の AI を下記のプログラムのように 簡潔定義できる ようになります。なお、この後で、実装した AI正しく動作するか どうかを 確認 するために、ai8s と対戦を行います。そこで、下記のプログラムでは、ai8s区別 できるように、ai8s2 という 名前 で関数を 定義 しています。なお、ai8s からの修正箇所がかなり多いので、修正箇所は省略します。

  • 11 行目enum_markpats メソッドを呼び出して、マークのパターン列挙 した list計算 し、markpats代入 する
  • 13、14 行目:「自 0、敵 2、空 1」の マークのパターン存在する 場合は、相手が勝利できる局面なので、-1評価値 として 返す
  • 16、17 行目:「自 2、敵 0、空 1」の マークのパターン存在する 場合は、自分の手番自分勝利できる 局面なので、1評価値 として 返す
  • 20 行目すべての条件満たさない 場合は、0評価値 として 返す
 1  def ai8s2(mb, debug=False):
 2     def eval_func(mb):
 3         # 真ん中のマスに着手している場合は、評価値として 2 を返す
 4         if mb.last_move == (1, 1):
 5             return 3
 6   
 7         # 自分が勝利している場合は、評価値として 1 を返す
 8         if mb.status == mb.last_turn:
 9             return 2
10
11         markpats = mb.enum_markpats()
12         # 相手が勝利できる場合は評価値として -1 を返す
13         if {mb.turn: 2, Marubatsu.EMPTY: 1} in markpats:
14             return -1
15         # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
16         elif {mb.last_turn: 2, Marubatsu.EMPTY: 1} in markpats:
17             return 1
18         # それ以外の場合は評価値として 0 を返す
19         else:
20             return 0
21
22     return ai_by_score(mb, eval_func, debug=debug)  
行番号のないプログラム
def ai8s2(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 2 を返す
        if mb.last_move == (1, 1):
            return 3
    
        # 自分が勝利している場合は、評価値として 1 を返す
        if mb.status == mb.last_turn:
            return 2

        markpats = mb.enum_markpats()
        # 相手が勝利できる場合は評価値として -1 を返す
        if {mb.turn: 2, Marubatsu.EMPTY: 1} in markpats:
            return -1
        # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
        elif {mb.last_turn: 2, Marubatsu.EMPTY: 1} in markpats:
            return 1
        # それ以外の場合は評価値として 0 を返す
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)  

動作の確認

ai8s2正しく動作 するかどうかを 確認 するために、ai8s対戦 を行います。実行結果 から、ai8s2正しく実装 できていることが 確認 できます。

ai_match(ai=[ai8s2, ai8s])

実行結果(実行結果はランダムなので下記とは異なる場合があります)

ai8s2 VS ai8s
count     win    lose    draw
o        3339     412    6249
x         405    3389    6206
total    3744    3801   12455

ratio     win    lose    draw
o       33.4%    4.1%   62.5%
x        4.0%   33.9%   62.1%
total   18.7%   19.0%   62.3%

ai8sai8s2 の処理速度の比較

先程説明したように、ai8s は特定の マークのパターンあるか どうかを 判定 する処理を 2 回 行っているので、その処理を 1 回 しか行わない ai8s2 より も処理に 時間がかかる ことが 予想 されます。そこで、実際に ai8s どうし と、ai8s2 どうし対戦 を行い、処理時間比較 することにします。

下記の ai8s どうし の対戦は、筆者のパソコンでは 49.1 秒 かかりました。

ai_match(ai=[ai8s, ai8s])

下記の ai8s2 どうし の対戦は、筆者のパソコンでは 45.5 秒 かかりました。

ai_match(ai=[ai8s2, ai8s2])

予想に反して 処理時間10% 程度 しか変わりませんでしたが、ai8s2 のほう処理時間短い ことは 確認 できました。処理時間が 大きく変わらない理由 は、ai8sマークのパターン見つかった時点処理を中断 するのに対し、ai8s必ずすべてマークのパターン計算する からです。

これで ai8s用済み なので、ai8s を破棄 して、ai8s2名前ai8s に変更 します。

ルールベースの AI の性質

ルールベースの AI の作成をかなり進めてきましたので、ここで、以前の記事で説明した ルールベース の AI の 性質 について具体例を元に 説明 します。

ルール 5、6 で付け加えた「勝てる場合勝つ」と「相手勝利できる 着手を 行わない」という 2 つの条件 は、以前の記事検証 したように、その条件を満たすような着手を行うことで、AI絶対に強くなる ことが 保証 されています。

一方、ルール 3 の「真ん中 のマスに 優先的着手 する」は、以前の記事で、有利である可能性が高い ことの 考察 は行いましたが、その条件を満たす着手がどのような場合でも 最善手である という 証明 までは 行っていません。それは、今回の ルール 8 で付け加えた 条件 でも 同様 です。例えば、一般的 にはそのルールに従うと 有利になる が、特定の局面 ではそのルールに従うと 不利になる ような 局面存在するかも しれません。

そのような、厳密強くなること保証されていない ルールで AI を 実装しても大丈夫なのか と思う人がいると思いますが、厳密強くなること保証される ような ルールを作成 することは 不可能場合が多い のが 現実 です。

そのことは、オセロ、将棋、囲碁のような、ある程度以上 複雑なゲーム を遊んだことがある人であれば、どのような局面 でも 適用できる絶対に強い手選択できるルールほとんど存在しない ことから理解できるのではないでしょうか。

従って、ルールベースAI を作成 する際には、本記事でこれまでに行ったように、下記のような 試行錯誤AI を強くする という手法を取ります。

  1. 有効だと考えられる ルールを 考案 する
  2. そのルールを 実装した AI作成 する
  3. 他の AI と対戦 させた 結果 から 有効であるか どうかを 判断 する
  4. 有効であれば採用 し、そうでなければ破棄 する
  5. 可能であれば、対戦結果などから作成した ルール の性質考察 することでゲームの 知識を深め次のルール作成 する際の 参考 にする

ルールベースAI を作成 する際には、以下 のような 難しさ があります。下記のうちのいくつかは、本記事で体験済です。

  • 最初は 思いついたルールで AIどんどん強くなっていく が、次第 に AI の 強さ がほとんど 向上しなくなっていく
  • ルールの アイディアが尽きてくる と、新しいルール考える のが 困難になる
  • 特定のルールのみ強い というルールを、一般的に強い ルールであると 勘違い してしまうと、強くしたつもりが 実際には弱くなる 場合がある
  • ルール数が多くなる と、複数のルールの間で 矛盾が生じる ようになり、ルール間の 優先順位 などの 整合性を取る ことが 困難 になる
  • ルール数が多くなる と、ルール単独で適用 すると 強くなる が、複数のルール組み合わせる と逆に 弱くなってしまう ような 状況が生じる

従って、最近 の AI人間ルールを考える というルールベースではなく、AI が 自動的ルール発見 する、機械学習 という手法を使ったものが主流になっています。〇×ゲームの AI を機械学習で作成する方法については、今後の記事で紹介します。

なお、〇×ゲーム は、単純 なゲームなので、ルールベース でも 最強の AI を作る ことが 可能 なので、引き続きルールベースで 〇×ゲームの AI を作成します。

今回の記事のまとめ

今回の記事では、ゲームに 勝つため必要な条件考察 し、ルール 8定義 しました。また、ルール 8実装 するために、マークのパターン列挙 する 関数を定義 しました。

今回の記事では、マークのパターン列挙list を使って行いましたが、次回の記事では、list 以外 の方法で マークのパターン列挙 する 方法 について説明します。

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

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

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

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

次回の記事

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?