1
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を一から作成する その62 条件の統合と乱数の種によるプログラムの再現性

Last updated at Posted at 2024-03-14

目次と前回の記事

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

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

これまでに作成した AI

これまでに作成した AI の アルゴリズム は以下の通りです。

ルール アルゴリズム
ルール1 左上から順空いているマス を探し、最初に見つかったマス着手 する
ルール2 ランダム なマスに 着手 する
ルール3 真ん中 のマスに 優先的着手 する
既に 埋まっていた場合ランダム なマスに 着手 する
ルール4 真ん中 のマスの 優先的着手 する
既に 埋まっていた場合ランダム なマスに 着手 する
ルール5 勝てる場合勝つ
そうでない場合は ランダム なマスに 着手 する
ルール6 勝てる場合勝つ
そうでない場合は 相手の勝利阻止 する
そうでない場合は ランダム なマスに 着手 する
ルール6改 勝てる場合勝つ
そうでない場合は 相手勝利できる 着手を 行わない
そうでない場合は ランダム なマスに 着手 する
ルール7 真ん中 のマスに 優先的着手 する
そうでない場合は 勝てる場合勝つ
そうでない場合は 相手の勝利阻止 する
そうでない場合は ランダム なマスに 着手 する
ルール7改 真ん中 のマスに 優先的着手 する
そうでない場合は 勝てる場合勝つ
そうでない場合は 相手勝利できる 着手を 行わない
そうでない場合は ランダム なマスに 着手 する
ルール8 真ん中 のマスに 優先的着手 する
そうでない場合は 勝てる場合勝つ
そうでない場合は 相手勝利できる 着手を 行わない
そうでない場合は、自分の手番勝利できる ように、「自 2 敵 0 空 1」が 1 つ以上 存在する 局面になる着手を行う
そうでない場合は ランダム なマスに 着手 する
ルール9 真ん中 のマスに 優先的着手 する
そうでない場合は 勝てる場合勝つ
そうでない場合は 相手勝利できる 着手を 行わない
そうでない場合は、自分の手番必ず勝利できる ように、「自 2 敵 0 空 1」が 2 つ以上存在する 局面になる着手を行う
そうでない場合は、自分の手番勝利できる ように、「自 2 敵 0 空 1」が 1 つ存在する 局面になる着手を行う
そうでない場合は ランダム なマスに 着手 する
ルール10 真ん中 のマスに 優先的着手 する
そうでない場合は 勝てる場合勝つ
そうでない場合は 相手勝利できる 着手を 行わない
そうでない場合は、自分の手番必ず勝利できる ように、「自 2 敵 0 空 1」が 2 つ以上存在する 局面になる着手を行う
そうでない場合は、以下 の 2 つを 総合的に判断 して着手を行う
  • 自分の手番勝利できる ように、「自 2 敵 0 空 1」が 1 つ存在する 局面になる着手を行う
  • 自分有利になる ように、「自 1 敵 0 空 2」が 最も多い 着手を行う
そうでない場合は ランダム なマスに 着手 する
ルール11 真ん中 のマスに 優先的着手 する
そうでない場合は 勝てる場合勝つ
そうでない場合は 相手勝利できる 着手を 行わない
そうでない場合は、自分の手番必ず勝利できる ように、「自 2 敵 0 空 1」が 2 つ以上存在する 局面になる着手を行う
そうでない場合は、以下 の 3 つを 総合的に判断 して着手を行う
  • 自分の手番勝利できる ように、「自 2 敵 0 空 1」が 1 つ存在する 局面になる着手を行う
  • 自分有利になる ように、「自 1 敵 0 空 2」が 最も多い 着手を行う
  • 相手不利になる ように、「自 0 敵 1 空 2」が 最も少ない 着手を行う
そうでない場合は ランダム なマスに 着手 する
ルール12 真ん中 のマスに 優先的着手 する
そうでない場合は 勝てる場合勝つ
そうでない場合は 相手勝利できる 着手を 行わない
そうでない場合は、自分の手番必ず勝利できる ように、「自 2 敵 0 空 1」が 2 つ以上存在する 局面になる着手を行う
そうでない場合は、斜め方向〇×〇並び他の 6 マス空のマス の場合に、いずれか辺のマス に着手を行う
そうでない場合は、以下 の 3 つを 総合的に判断 して着手を行う
  • 自分の手番勝利できる ように、「自 2 敵 0 空 1」が 1 つ存在する 局面になる着手を行う
  • 自分有利になる ように、「自 1 敵 0 空 2」が 最も多い 着手を行う
  • 相手不利になる ように、「自 0 敵 1 空 2」が 最も少ない 着手を行う
そうでない場合は ランダム なマスに 着手 する

ルール 11、12評価値を計算 する際の パラメータ は以下の通りです。

ai11s
ver 1
ai11s
ver 2
ai11s ver 3
ai12s
「自 2 敵 0 空 1」が 1 つの場合の評価値 1 2 2
「自 1 敵 0 空 2」が 1 つあたりの評価値 1 1 0.5
「自 0 敵 1 空 2」が 1 つあたりの評価値 -1 -1 1

基準となる 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
ai8s 98.2 0.1 1.6 89.4 2.5 8.1 93.8 1.3 4.9
ai9s 98.7 0.1 1.2 89.6 2.4 8.0 94.1 1.3 4.6
ai10s 97.4 0.0 2.6 85.6 2.6 11.7 91.5 1.3 7.2
ai11s ver 1 98.1 0.0 1.9 82.5 1.9 15.6 90.3 1.0 8.7 あり
ai11s ver 2 98.8 0.0 1.2 87.7 2.4 10.0 93.2 1.2 5.6
ai11s ver 3 99.1 0.0 0.9 87.7 0.8 11.5 93.4 0.4 6.2
ai12s 98.9 0.0 1.1 88.2 0.0 11.8 93.5 0.0 6.5

前回の記事のおさらい

前回の記事で、ルールベースの AI として、ai12s という、弱解決の AI が完成しました。

今回の記事から、ルールベースの AI に関するいくつかの 補足説明 を行います。

ルールの条件の統合

ルールベースの AI は、ルール条件 が増えれば 増えるほど複雑になる ため、わかりづらく なったり、複数の条件 の間で 矛盾が生じたりする ようになります。そのため、なるべく 条件の数少ない ほうが 望ましい と言えるので、複数の条件 の中で、一つにまとめる ことが できる ものがあれば、それらを 統合 したほうが 良いでしょう

ルール 12 の条件の統合

実は、ルール 12条件 の中の、最初の「真ん中 のマスに 優先的着手 する」という 条件 は、ルール 12下記の条件 があれば 削除 しても 真ん中 のマスに 優先的着手行われる ので、下記の条件統合 することが できます

  • 自分の手番勝利できる ように、「自 2 敵 0 空 1」が 1 つ存在する 局面になる着手を行う
  • 自分有利になる ように、「自 1 敵 0 空 2」が 最も多い 着手を行う
  • 相手不利になる ように、「自 0 敵 1 空 2」が 最も少ない 着手を行う

統合できる理由 について 説明 します。

ai12s の修正と確認

まず、下記 のプログラムのように、ai12s から、「真ん中 のマスに 優先的着手 する」という 条件を処理 する 部分を削除 します。以後は、修正前ai12sai12s ver1修正後ai12s ver 2表記 することにします。

from marubatsu import Markpat, Marubatsu
from ai import ai_by_score
from pprint import pprint

def ai12s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):    
    def eval_func(mb):      
        # ここにあった、真ん中のマスに着手した場合に評価値として 400 を返す処理を削除する
        
        # 自分が勝利している場合は、評価値として 300 を返す
        if mb.status == mb.last_turn:
            return 300
元と同じなので省略
プログラム全体
from marubatsu import Markpat, Marubatsu
from ai import ai_by_score
from pprint import pprint

def ai12s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):    
    def eval_func(mb):      
        # 自分が勝利している場合は、評価値として 300 を返す
        if mb.status == mb.last_turn:
            return 300

        markpats = mb.count_markpats()
        if debug:
            pprint(markpats)
        # 相手が勝利できる場合は評価値として -100 を返す
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
            return -100
        # 次の自分の手番で自分が必ず勝利できる場合は評価値として 200 を返す
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
            return 200
        
        # 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が
        # 配置されている場合は評価値として 100 を返す
        if mb.board[1][1] == Marubatsu.CROSS and \
           (mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
            mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
           (mb.board[1][0] == Marubatsu.CROSS or \
            mb.board[0][1] == Marubatsu.CROSS or \
            mb.board[2][1] == Marubatsu.CROSS or \
            mb.board[1][2] == Marubatsu.CROSS) and \
           mb.move_count == 4:
            return 100              

        # 評価値の合計を計算する変数を 0 で初期化する
        score = 0        
        # 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
        if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
            score += score_201
        # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
        score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
        # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
        score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
        
        # 計算した評価値を返す
        return score

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

def ai12s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):    
    def eval_func(mb):      
        # 真ん中のマスに着手している場合は、評価値として 400 を返す
-       if mb.last_move == (1, 1):
-           return 400
            
        # 自分が勝利している場合は、評価値として 300 を返す
        if mb.status == mb.last_turn:
            return 300
元と同じなので省略

次に、ai12s ver 2 が、真ん中のマス優先的着手を行う ことを 検証 します。そのためには、〇 の手番× の手番それぞれを担当 する場合の 最初の手番 である、ゲーム開始時局面 と、2 手目局面 について 調べる必要あります

ゲーム開始時の局面での ai12s ver 2 の合法手の選択の検証

ゲーム開始時ai12s ver 2選択 する 合法手下記 のプログラムで 検証 します。

mb = Marubatsu()
ai12s(mb, debug=True)
実行結果(長いのでクリックして開いてください)
Start ai_by_score
Turn o
...
...
...

legal_moves [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
====================
move (0, 0)
Turn x
O..
...
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 5,
             Markpat(last_turn=1, turn=0, empty=2): 3})
score 1.5 best score -inf
UPDATE
  best score 1.5
  best moves [(0, 0)]
====================
move (1, 0)
Turn x
.O.
...
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 6,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score 1.0 best score 1.5
====================
move (2, 0)
Turn x
..O
...
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 5,
             Markpat(last_turn=1, turn=0, empty=2): 3})
score 1.5 best score 1.5
APPEND
  best moves [(0, 0), (2, 0)]
====================
move (0, 1)
Turn x
...
O..
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 6,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score 1.0 best score 1.5
====================
move (1, 1)
Turn x
...
.O.
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=1, turn=0, empty=2): 4})
score 2.0 best score 1.5
UPDATE
  best score 2.0
  best moves [(1, 1)]
====================
move (2, 1)
Turn x
...
..O
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 6,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score 1.0 best score 2.0
====================
move (0, 2)
Turn x
...
...
O..

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 5,
             Markpat(last_turn=1, turn=0, empty=2): 3})
score 1.5 best score 2.0
====================
move (1, 2)
Turn x
...
...
.O.

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 6,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score 1.0 best score 2.0
====================
move (2, 2)
Turn x
...
...
..O

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 5,
             Markpat(last_turn=1, turn=0, empty=2): 3})
score 1.5 best score 2.0
====================
Finished
best score 2.0
best moves [(1, 1)]

下記は、上記の結果まとめた表 です。なお、表の それぞれの合法手 には 1 つの局面 しか 表記 していませんが、合法手 23 にはそれぞれ 4 つの同一局面存在 します。

「201」 「021」 「102」 「012」 評価値
評価値 1:+2
2~:200
-100 1 つで
+0.5
1 つで
-1
1 4 2
2 隅のマス
3 1.5
3 辺のマス
2 1

上記の表 から、真ん中(1, 1) のマスを表す 合法手 1評価値2最も高くなる ことがわかります。そのことは、下記の 実行結果最後の 2 行 からも 確認 できます。

best score 2.0
best moves [(1, 1)]

また、下記の性質 から、ai12s ver 2パラメータどのように設定 しても、真ん中のマスを 表す 合法手 1評価値のみ最も高くなる ことが わかります

  • 3 つの合法手 はいずれも、「自 1 敵 0 空 2」の マークのパターンのみ存在 する
  • 自 1 敵 0 空 2」の マークのパターン は、合法手 1 のみ最も大きい
  • 以前の記事 で説明したように、「自 1 敵 0 空 2」の マークのパターンパラメータ は、正の値 である 必要がある

上記から、ai12s ver 2 は、最初の手番 である ゲーム開始時局面 で、真ん中のマス優先的着手を行う ことが 確認 できました。

2 手目の局面での ai12s ver 2 の合法手の選択の検証

2 手目の局面 は、同一局面考慮 すると、下記の 3 種類局面あります

このうち、左の局面 は、真ん中 のマスに マークが配置済 なので 検証する必要ありません ので、残りの 2 つ の局面で 検証 を行います。

1 手目で隅のマスに 〇 が配置された場合

まず、下記 のプログラムで、1 手目隅のマス〇 が配置 された場合を 検証 します。

mb = Marubatsu()
mb.move(0, 0)
ai12s(mb, debug=True)
実行結果(長いのでクリックして開いてください)
Start ai_by_score
Turn x
O..
...
...

legal_moves [(1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
====================
move (1, 0)
Turn o
oX.
...
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 1,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -1.5 best score -inf
UPDATE
  best score -1.5
  best moves [(1, 0)]
====================
move (2, 0)
Turn o
o.X
...
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -1.0 best score -1.5
UPDATE
  best score -1.0
  best moves [(2, 0)]
====================
move (0, 1)
Turn o
o..
X..
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 1,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -1.5 best score -1.0
====================
move (1, 1)
Turn o
o..
.X.
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 2,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 3,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -0.5 best score -1.0
UPDATE
  best score -0.5
  best moves [(1, 1)]
====================
move (2, 1)
Turn o
o..
..X
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 3,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score -2.0 best score -0.5
====================
move (0, 2)
Turn o
o..
...
X..

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -1.0 best score -0.5
====================
move (1, 2)
Turn o
o..
...
.X.

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 3,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score -2.0 best score -0.5
====================
move (2, 2)
Turn o
o..
...
..X

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -1.0 best score -0.5
====================
Finished
best score -0.5
best moves [(1, 1)]

下記は、上記の結果まとめた表 です。

「201」 「021」 「102」 「012」 評価値
評価値 1:+2
2~:200
-100 1 つで
+0.5
1 つで
-1
1 1 2 -1.5
2 2 2 -1
3 3 2 -0.5
4 2 3 -2
5 2 2 -1

上記の表 から、真ん中(1, 1) のマスを表す 合法手 3評価値-0.5最も高くなる ことが わかります。そのことは、下記の 実行結果最後の 2 行 からも 確認 できます。

best score -0.5
best moves [(1, 1)]

また、下記の性質 から、ai12s ver 2パラメータどのように設定 しても、真ん中のマスを 表す 合法手 3評価値のみ最も高くなる ことが わかります

  • 3 つ合法手いずれも、「自 1 敵 0 空 2」と「自 0 敵 1 空 2」の マークのパターンのみ存在 する
  • 自 1 敵 0 空 2」の マークのパターン は、合法手 3 のみ最も大きい
  • 以前の記事 で説明したように、「自 1 敵 0 空 2」の マークのパターンパラメータ は、正の値 である 必要がある
  • 自 0 敵 1 空 2」の マークのパターン は、合法手 3最も小さい
  • 以前の記事 で説明したように、「自 0 敵 1 空 2」の マークのパターンパラメータ は、負の値 である 必要がある

上記の性質 の中で、「自 1 敵 0 空 2」の マークのパターン最大 になる 合法手 が、合法手 3 のみ であるという 性質特に重要 です。この性質なければ合法手 3 以外合法手評価値が最大 になる 可能性が生じる からです。

1 手目で辺のマスに 〇 が配置された場合

次に、下記 のプログラムで、1 手目辺のマス〇 が配置 された場合を 検証 します。

mb = Marubatsu()
mb.move(1, 0)
ai12s(mb, debug=True)
実行結果(長いのでクリックして開いてください)
Start ai_by_score
Turn x
.O.
...
...

legal_moves [(0, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
====================
move (0, 0)
Turn o
Xo.
...
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 1,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score 0.0 best score -inf
UPDATE
  best score 0.0
  best moves [(0, 0)]
====================
move (2, 0)
Turn o
.oX
...
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 1,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score 0.0 best score 0.0
APPEND
  best moves [(0, 0), (2, 0)]
====================
move (0, 1)
Turn o
.o.
X..
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score -1.0 best score 0.0
====================
move (1, 1)
Turn o
.o.
.X.
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 1,
             Markpat(last_turn=1, turn=0, empty=2): 3,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score 0.5 best score 0.0
UPDATE
  best score 0.5
  best moves [(1, 1)]
====================
move (2, 1)
Turn o
.o.
..X
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score -1.0 best score 0.5
====================
move (0, 2)
Turn o
.o.
...
X..

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 3})
score -0.5 best score 0.5
====================
move (1, 2)
Turn o
.o.
...
.X.

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 5,
             Markpat(last_turn=0, turn=1, empty=2): 1,
             Markpat(last_turn=1, turn=0, empty=2): 1,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -0.5 best score 0.5
====================
move (2, 2)
Turn o
.o.
...
..X

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 3})
score -0.5 best score 0.5
====================
Finished
best score 0.5
best moves [(1, 1)]

下記は、上記の結果まとめた表 です。

「201」 「021」 「102」 「012」 評価値
評価値 1:+2
2~:200
-100 1 つで
+0.5
1 つで
-1
1 2 1 0
2 2 2 -1
3 3 1 0.5
4 3 2 -0.5
5 1 1 -0.5

上記の表 から、真ん中(1, 1) のマスを表す 合法手 3評価値0.5最も高くなる ことがわかります。そのことは、下記の 実行結果最後の 2 行 からも 確認 できます。

best score 0.5
best moves [(1, 1)]

また、下記の性質 から、ai12s ver 2パラメータどのように設定 しても、真ん中のマスを 表す 合法手 3評価値のみ最も高くなる ことが わかります

  • 3 つ合法手いずれも、「自 1 敵 0 空 2」と「自 0 敵 1 空 2」の マークのパターンのみ存在 する
  • 自 1 敵 0 空 2」の マークのパターン は、合法手 3最も大きい
  • 以前の記事 で説明したように、「自 1 敵 0 空 2」の マークのパターンパラメータ は、正の値 である 必要がある
  • 自 0 敵 1 空 2」の マークのパターン は、合法手 3最も小さい
  • 以前の記事 で説明したように、「自 0 敵 1 空 2」の マークのパターンパラメータ は、負の値 である 必要がある
  • 自 1 敵 0 空 2」の マークのパターン最大 で、なおかつ「自 0 敵 1 空 2」の マークのパターン最小 なのは、合法手 3 のみ である

上記の性質 は、先程の「1 手目隅のマス〇 が配置 された場合」の 性質似ているように見える かもしれませんが、下記 の点で 異なります

  • 自 1 敵 0 空 2」の マークのパターン最大 である 合法手複数ある
  • 最後の性質追加 されている

最後の性質追加 したのは、この性質なければ合法手 3 以外合法手評価値が最大 になる 可能性が生じる からです。

以上検証 から、ai12s ver 2 は、× の手番 である 2 手目局面 で、真ん中マス優先的着手を行う ことが 確認 できました。

先程示したように、1 手目の局面 でも 真ん中マス優先的着手を行う ので、ai12s ver 2 は、常に真ん中のマス優先的着手を行う ことが わかりました

従って、ルール 12 から「真ん中 のマスに 優先的着手 する」という 条件削除しても同じ合法手選択される ので、この条件削除 しても かまいません

ルール 12 改の定義

ルール 12 から「真ん中 のマスに 優先的着手 する」という 条件削除 した、下記のルールを、ルール 12 改定義 する事にします。

順位 条件 種類
1 勝てる場合勝つ 十分条件
2 相手勝利できる 着手を 行わない 必要条件
3 自 2 敵 0 空 1」が 2 つ以上存在 する着手を行う 十分条件
4 斜め方向〇×〇並び他の 6 マス空のマス
場合は、いずれか辺のマス に着手を行う
十分条件
5 自 2 敵 0 空 1」が 1 つ存在 する着手を行う
自 1 敵 0 空 2」が 最も多い 着手を行う
自 0 敵 1 空 2」が 最も少ない 着手を行う
6 ランダム なマスに 着手 する

また、それぞれ条件 に対する 評価値下記 のようになります。

順位 局面の状況 個別 評価値
1 自分が勝利している 300
3 「自 2 敵 0 空 1」が 2 つ以上存在する 200
4 片方斜め方向〇×〇並びいずれか
1 つのマスのみ× が配置 されている
100
5 「自 2 敵 0 空 1」が 1 つ存在する
「自 1 敵 0 空 2」が x 個存在する
「自 0 敵 1 空 2」が y 個存在する
2 (0~2)
0.5 * x (0~4)
-y (-8~0)
-8~6
2 相手が勝利できる -100

ai12s ver 2 が弱解決の AI であることの確認

ai12s ver 2弱解決の AI であることを 確認 するために、下記 のプログラムで、ランダムな着手 を行う ai2対戦 させてみます。

from ai import ai_match, ai2

ai_match(ai=[ai12s, ai2])

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

ai12s VS ai2
count     win    lose    draw
o        9905       0      95
x        8803       0    1197
total   18708       0    1292

ratio     win    lose    draw
o       99.1%    0.0%    0.9%
x       88.0%    0.0%   12.0%
total   93.5%    0.0%    6.5%

実行結果 から、敗率0 % であることが 確認 できたので、ai12s ver 2弱解決の AI である 可能性が高い ことが わかりました

下記は 上記対戦結果 に、ai12s ver 1 VS ai2対戦結果加えた ものです。対戦成績ほぼ同じ なので、ai12s ver 1ai12s ver 2同じ合法手選択 する AI である 可能性非常に高い ことが 確認 できました。

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分
ai12s ver 1 98.9 0.0 1.1 88.2 0.0 11.8 93.5 0.0 6.5
ai12s ver 2 99.1 0.0 0.9 88.0 0.0 12.0 93.5 0.0 6.5

最善手の優劣

これまでの説明では、局面最善手複数存在 した場合は、それらの中 から 同じ確率平等 に、ランダム最善手選択 していました。しかし、複数最善手存在 した場合に、その中で 優劣存在しない考えて良いでしょうか最強の AI作成 するという 目的だけ考える のであれば、最善手 の間に 優劣ありません が、別の観点 から 見ると最善手優劣存在 する場合が あります。本記事では、必勝の局面必敗の局面2 つの場合1 での、複数の最善手 の間の 違い について説明します。

なお、ここでいう 最善手 とは、演繹法 による 条件 によって、最善手 であることが 保証されている ものの事です。ヒューリスティックスな条件 によって 求められた評価値最も高い合法手 の事 ではない 点に 注意 して下さい。

必勝の局面での最善手の差異

ルール 12 改条件 の中で、「勝てる場合勝つ」と、『「自 2 敵 0 空 1」が 2 つ以上存在 する着手を行う』という 条件 は、それら を満たす 合法手を選択 することで、必ず勝利できる ので、いずれも 必勝の局面最善手選択 するという 条件 です。ai12s ver 2 では、それらに対する 評価値 を、先程示した表のように、300200 という、異なる値設定 していたので、それらの条件 を満たす 合法手どちらも存在 する場合は、評価値高い勝てる場合勝つ」ほうの 合法手選択される ことになります。

しかし、どちらの条件 による 合法手最善手である ことに 変わりはない ので、同じ評価値設定 しても、弱解決の AI であることは 変わりませんそのことを示す ために、その 2 つの条件評価値同じ値 にして ランダムな AI対戦 してみることにします。

ai12s の修正

上記の 2 つの 評価値修正 する際に、毎回 ai12sプログラム変更 するのは 大変 なので、「勝てる場合勝つ」と、『「自 2 敵 0 空 1」が 2 つ以上存在 する着手を行う』に対する 評価値 も、パラメータ として 実引数設定できる ように ai12s修正する ことにします。また、せっかくなので、他の評価値実引数設定 できるようにします。

それぞれの パラメータを代入 する 仮引数 の名前は、下記 のように 命名 しました。

局面の状況 仮引数の名前 仮引数の名前の由来
自分が勝利している score_victory 勝利を表す victory
「自 2 敵 0 空 1」が
2 つ以上存在する
score_sure_victory 必勝を表す sure victory
片方斜め方向〇×〇
並びいずれか1 つの
マスのみ
× が配置 されている
score_special 特殊を表す special
相手が勝利できる score_defeat 敗北を表す defeat

下記は、上記の 仮引数 を、これまでのパラメータデフォルト値 として持つ デフォルト引数 として 追加 した ai12s のプログラムです。

  • 1、2 行目:上記の 仮引数デフォルト引数 として追加する
  • 7、14、17、28返り値仮引数修正 する
 1  def ai12s(mb, score_victory=300, score_sure_victory=200, score_defeat=-100,
 2            score_special=100, score_201=2, score_102=0.5, score_012=-1, debug=False):  3  
 4      def eval_func(mb):         
 5          # 自分が勝利している場合
 6          if mb.status == mb.last_turn:
 7              return score_victory
 8
 9          markpats = mb.count_markpats()
10          if debug:
11              pprint(markpats)
12          # 相手が勝利できる場合
13          if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
14              return score_defeat
15          # 次の自分の手番で自分が必ず勝利できる場合
16          elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
17              return score_sure_victory
18        
19          # 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
20          if mb.board[1][1] == Marubatsu.CROSS and \
21             (mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
22              mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
23             (mb.board[1][0] == Marubatsu.CROSS or \
24              mb.board[0][1] == Marubatsu.CROSS or \
25              mb.board[2][1] == Marubatsu.CROSS or \
26              mb.board[1][2] == Marubatsu.CROSS) and \
27             mb.move_count == 4:
28              return score_special       
以下同じなので略
行番号のないプログラム
def ai12s(mb, score_victory=300, score_sure_victory=200, score_defeat=-100,
          score_special=100, score_201=2, score_102=0.5, score_012=-1, debug=False):    
    def eval_func(mb):         
        # 自分が勝利している場合
        if mb.status == mb.last_turn:
            return score_victory

        markpats = mb.count_markpats()
        if debug:
            pprint(markpats)
        # 相手が勝利できる場合
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
            return score_defeat
        # 次の自分の手番で自分が必ず勝利できる場合
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
            return score_sure_victory
        
        # 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
        if mb.board[1][1] == Marubatsu.CROSS and \
           (mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
            mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
           (mb.board[1][0] == Marubatsu.CROSS or \
            mb.board[0][1] == Marubatsu.CROSS or \
            mb.board[2][1] == Marubatsu.CROSS or \
            mb.board[1][2] == Marubatsu.CROSS) and \
           mb.move_count == 4:
            return score_special    

        # 評価値の合計を計算する変数を 0 で初期化する
        score = 0        
        # 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
        if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
            score += score_201
        # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
        score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
        # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
        score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
        
        # 計算した評価値を返す
        return score

    return ai_by_score(mb, eval_func, debug=debug)
修正箇所
-def ai12s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):    
+def ai12s(mb, score_victory=300, score_sure_victory=200, score_defeat=-100,
+          score_special=100, score_201=2, score_102=0.5, score_012=-1, debug=False):    
    def eval_func(mb):         
        # 自分が勝利している場合
        if mb.status == mb.last_turn:
-           return 300
+           return score_victory

        markpats = mb.count_markpats()
        if debug:
            pprint(markpats)
        # 相手が勝利できる場合
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
-           return -100
+           return score_defeat
        # 次の自分の手番で自分が必ず勝利できる場合
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
-           return 200
+           return score_sure_victory
        
        # 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
        if mb.board[1][1] == Marubatsu.CROSS and \
           (mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
            mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
           (mb.board[1][0] == Marubatsu.CROSS or \
            mb.board[0][1] == Marubatsu.CROSS or \
            mb.board[2][1] == Marubatsu.CROSS or \
            mb.board[1][2] == Marubatsu.CROSS) and \
           mb.move_count == 4:
-           return -100
+           return score_special    
以下同じなので略

ランダムな AI との対戦

下記 のプログラムで、「自分が勝利 している場合」の 評価値 を、『「自 2 敵 0 空 1」が 2 つ以上存在 する場合』の 評価値同じ 200設定 して、ランダムな AI である ai2 と対戦 してみることにします。以後は、このパラメータAIai12s ver 3表記 します。

ai_match(ai=[ai12s, ai2], params=[{ "score_victory": 200 }, {}])

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

ai12s VS ai2
count     win    lose    draw
o        9577       0     423
x        8686       0    1314
total   18263       0    1737

ratio     win    lose    draw
o       95.8%    0.0%    4.2%
x       86.9%    0.0%   13.1%
total   91.3%    0.0%    8.7%

実行結果 から、敗率が 0 % であることが 確認 できたので、ai12s ver 3高確率弱解決の AI であると考えることができます。

下記は、これまでai12s VS ai2対戦結果加えた ものです。

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分
ai12s ver 1 98.9 0.0 1.1 88.2 0.0 11.8 93.5 0.0 6.5
ai12s ver 2 98.9 0.0 1.1 88.7 0.0 11.3 93.8 0.0 6.2
ai12s ver 3 95.8 0.0 4.2 86.9 0.0 13.1 91.3 0.0 8.7

上記 の表から、ai12s ver 3 では、それ 以前のバージョンより引き分け率高くなる ことがわかります。ai12s ver 3 と、それ 以前のバージョン違い異なる最善手選択 する 場合がある 点ですが、最善手選択 することに 変わりはない ので、上記のように、引き分け率多くなる のは 明らかに変 です。

実は、この現象 は、筆者 にとっても 想定外の現象 でした。原因調べてみた 所、「自分が勝利 している場合」の 評価値 を、『「自 2 敵 0 空 1」が 2 つ以上存在 する場合』の 評価値同じ値にする ことによって、思わぬバグが発生する ことが わかりました

この バグの原因調べる方法 は、プログラムデバッグ具体例 として 参考になる のではないかと思いましたので、最善手優劣 の話は、次回の記事 で行うことにし、今回の記事 では、予定を変更 してこのようなことが起きる 原因を検証 することにします。

ai12s ver 3 VS ai2 の検証

以前の記事で説明したように、ランダムな AI は、世の中存在 する すべての AI選択する着手 と、まったく同じ着手選択する可能性 があります。そのため、ランダムな AI何度も対戦する ことで、理論上すべての AI1 回以上対戦 することが できます

従って、ランダムな AI である ai2何度も対戦 した際に、ai12s ver 3 のほうが、ai12s ver 2 より引き分け率増えた ことは、ai12s ver 2対戦 すると 勝利 するが、ai12s ver 3対戦 すると 引き分け になる AIいくつか存在する ことを 意味 します。

そのような AI作る ことが できればその AIai12s ver 2ai12s ver 3 との 対戦経過違い検証 することで、引き分け増えた原因調べる ことが できます

そのような AI は、下記の手順見つける ことが できます

  1. 世の中に存在 する AI の中 から ランダムAI一つ選択 する
  2. ai12s ver 2 VS その AI対戦 を行い 結果を記録 する
  3. ai12s ver 3 VS その AI対戦 を行い 結果を記録 する
  4. 手順 23対戦結果同じ場合手順 1戻る
  5. 対戦結果異なる場合 は、その AI求める AI である

ランダムな AI対戦する ということは、世の中に存在 する AI の中 から ランダムAI一つ選択 して 対戦する ことを 意味する ので、上記の 手順 12ai2 と対戦 することで、簡単に行う ことが できます。しかし、ai2対戦行うたび に、ランダムAI選択し直す ので、残念ながら ai2 と対戦 することで 手順 3行うことできません

この問題解決 するためには、プログラムの再現性 について 理解 する 必要あります

ランダムな処理とプログラムの再現性

一般的に、ランダムな処理行わない プログラムは、行う処理関係する条件同じ状態 でプログラムを 実行 した場合は、毎回同じ処理行います。例えば、下記1 + 2ランダムな処理行わない ので、2 回実行 しても、画面に 必ず 3 が表示 されます。

print(1 + 2)
print(1 + 2)

実行結果

3
3

下記の add という 関数 は、ランダムな処理行わないの で、下記 のプログラムのように、同じ実引数記述 して 呼び出す と、必ず同じ数値表示 されます。

def add(x, y):
    print(x + y)

add(1, 2)
add(1, 2)

実行結果

3
3

このような、同じ条件実行 すると、必ず同じ処理 が行われる プログラム のことを、再現性がある プログラムと 呼びます

ランダムな処理行わない プログラムであっても、例えば 時間を表示 するプログラムのような 場合 などでは、一見すると 条件同じように見える にも関わらず、毎回異なる表示行われる 場合が あります。しかし、その場合は、プログラム実行する時間 という 条件が異なる ので、異なる処理行われます

なお、上記add という 関数 は、時間関する処理行わない ので、実行する時間異なっても、毎回 同じ処理行われます

一方、ai2 のように、ランダムな処理行う プログラムは、下記 のプログラムのように、実行するたびに 異なる処理行われます

mb = Marubatsu()
print(ai2(mb))
print(ai2(mb))

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

(1, 1)
(1, 2)

このような 同じ条件実行 しても 毎回異なる処理 を行う プログラム の事を、再現性のない プログラムと 呼びます

なお、プログラム では 乱数 を使って ランダムな処理行う ので、以後は ランダムな処理を行うプログラム の事を、乱数を使ったプログラム表記 することにします。

プログラムの再現性の重要性

バグが発生 した場合は、その バグの原因検証 して 取り除く必要 があります。バグ修正 するには 様々な方法あります が、プログラム見ても 原因が 良くわからない場合 は、前回の記事ai11sバグの原因調べる するために ai2 VS ai11s検証 を行ったように、バグが発生 する 状況再現 して 検証する という 方法良く使われます

その際に、再現性のある プログラム であればバグが発生 した場合と 同じ条件 でその プログラムを実行 すると 同じバグ発生 するので、バグの原因検証 することが できます

一方、乱数使ったプログラム には、一般的に 再現性がない ため、バグが発生 した 場合 に、その バグの原因検証 することが 難しい という 問題 があります。その 理由 は、特定の乱数発生した場合だけエラーが発生する ような バグ の場合は、もう一度 その プログラム実行 しても、その エラーが発生しない可能性が高い からです。

例えば、100 万分の 1確率バグが発生する ような プログラム があった場合に、バグの原因検証 しようと思っても、再び 同じバグが発生 するまで、そのプログラムを 約 100 万回実行する必要あります

乱数の種を使った、再現性のある乱数を利用するプログラム

乱数を利用 するプログラムにも 再現性持たせる ことが できれば上記の問題解決 することが できます。また、先ほどの 手順 23 で、ai12s ver 2ai12s ver 3 が、手順 1ランダムに選択 した 同一の AI対戦 することが できるようなります

乱数再現性矛盾 する要素だと 思う人いるかもしれません が、ここでいう 再現性とは一度発生 させた 複数の乱数 と、全く同じパターン乱数何度でも発生 させて 利用する ことが できる という 意味 での 再現性 です。

例えば、サイコロ10 回 振った際の 出目記録 しておき、サイコロ振る必要生じた際 に、過去振ったサイコロ出目再利用 するということに 相当 します。

このような 乱数再現性 は、乱数の種利用 することで 実現 することが できます乱数の種意味使い方理解 するために、乱数の仕組み について 説明 します。

乱数の仕組み

コンピューター発生 させる 乱数 は、実は 本当の意味 での 乱数ではなく計算 によって 求められた、乱数のように見える 疑似乱数 と呼ばれるものです。それに対して、本当の意味 での でたらめな数 のことを、真の乱数 と呼びます。残念ながら、数学計算 は、同じデータ に対して 同じ計算 を行うと、同じ計算結果得られる ため、コンピュータ計算 によって 真の乱数発生 させることは できません

真の乱数発生 させるためには、特別な装置(ハードウェア) が 必要 ですが、一般的なコンピュータ には そのような装置備わっていません

疑似乱数最も簡単作り方一つ に、下記手順乱数を計算 する、線形合同法 という アルゴリズム があります。興味がある方は、下記に線形合同法のリンクを紹介しますので、そちらを見て下さい。

  1. 整数 $A$、$B$、$M$ を 何らかの値定める。ただし、$M > A$、$M > B$、$A > 0$、$B >= 0$ で、 $A$、$B$、$M$ は 定数2である

  2. 乱数の種 と呼ばれる 整数 $X_0$ を 決める。これは、任意の値設定 しても 良い

  3. 以下計算式乱数 $X_n$ を 計算 する。mod余り計算する という 演算子 である

    $X_{n+1} = (A × X_n + B) mod M$

具体例 を挙げます。

$A = 5、B = 0、M = 7、X_0 = 1$ とした場合、下記手順 で、546231546231、・・・ という 乱数次々に計算 することが できます

$X_1 = 5 × 1 mod 7 = 5 mod 7 = (0 × 7 + 5) mod 7 = 5$
$X_2 = 5 × 5 mod 7 = 25 mod 7 = (3 × 7 + 4) mod 7 = 4$
$X_3 = 5 × 4 mod 7 = 20 mod 7 = (2 × 7 + 6) mod 7 = 6$
$X_4 = 5 × 6 mod 7 = 30 mod 7 = (4 × 7 + 2) mod 7 = 2$
$X_5 = 5 × 2 mod 7 = 10 mod 7 = (1 × 7 + 3) mod 7 = 3$
$X_6 = 5 × 3 mod 7 = 15 mod 7 = (2 × 7 + 1) mod 7 = 1$
$X_7 = 5 × 1 mod 7 = 5 mod 7 = (0 × 7 + 5) mod 7 = 5$
・・・以下 同様計算繰り返される

下記は、上記の乱数計算 する プログラム です。実行結果 から、上記と同じ乱数計算 できていることが わかります

A = 5
B = 0
M = 7
X = 1
for _ in range(10):
    X = A * X % M
    print(X, end=",")

実行結果

5,4,6,2,3,1,5,4,6,2,

この例 から わかるよう に、線形合同法 によって 計算 された 疑似乱数 には、同じパターン乱数繰り返される という 周期性あります。ただし、$M$ を 大きな値 にすることで、周期性 があることが わかりづらい長い周期疑似乱数計算できます

線形合同法 は、計算簡単 なため 計算時間短くコンピューターの性能 が今のコンピューターと比較して 何万分の一以下 だった 2000 年以前 の頃は 良く使われていました が、疑似乱数品質が悪い偏った乱数作られてしまう という 欠点 があるので、最近 では あまり使われません。Python の random モジュール など、最近プログラム言語 では もっと複雑な計算式 で、品質の良い疑似乱数計算 する 関数用意されています

参考までに、乱数に関する Wikipedia のリンクを紹介します。

乱数の種

線形合同法 の $X_0$ のように、乱数計算 する際に、最初任意の数値 として 設定できる値 のことを、乱数の種 と呼び、どのような疑似乱数アルゴリズム であっても、疑似乱数計算し始める際 に、何らかの値設定する必要あります疑似乱数 は、乱数の種計算 して 作られる ので、同じ乱数の種 から 計算 された 疑似乱数 は、毎回 同じパターン疑似乱数発生 させます。つまり、乱数の種 を使うことで、乱数使った プログラムに 再現性持たせる ことが できる ようになります。

乱数の種の設定方法

Python の random モジュール には、乱数の種設定 する seed という 関数 があり、この関数の 実引数乱数の種 を表す 整数 を記述して呼び出すことで、乱数の種設定 することが できます。また、同じ乱数の種設定 することで、その後発生 する 乱数毎回同じパターン になるという、乱数再現性実現 することが できます

具体例 を挙げます。random モジュール には、指定した範囲整数乱数発生 させる randint という 関数定義 されており、例えば randint(1, 6)実行 すると、1 から 6 までの ランダムな整数 を、サイコロの ように 発生 させることが できます

下記 は、1 ~ 6 までの 整数の乱数10 回 計算して 表示する という 処理 を、乱数の種設定せず3 回その後乱数の種0設定 した後で、3 回 行うプログラムです。

実行結果 から、乱数の種設定しない場合毎回異なる 10 個の 疑似乱数表示 されますが、乱数の種0設定した後表示 される 疑似乱数 は、3 回 とも 全く同じパターン になることが 確認 できます。

import random

for i in range(3):
    for j in range(10):
        print(random.randint(1, 6), end=",")
    print()

for i in range(3):
    random.seed(0)
    for j in range(10):
        print(random.randint(1, 6), end=",")
    print()

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

5,4,2,2,4,1,5,6,5,2,
4,3,5,1,6,5,6,5,5,3,
1,1,2,3,2,3,2,4,2,4,
4,4,1,3,5,4,4,3,4,3,
4,4,1,3,5,4,4,3,4,3,
4,4,1,3,5,4,4,3,4,3,

上記 のプログラムで、これまで のプログラムと 同様 に、seedrandint を下記のプログラムのように 個別インポートしない理由 は、この後 のプログラムで、seed という 名前変数利用するため です。

from random import seed, randint

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

乱数の種を設定しない場合に行われる処理

これまで のプログラムでは、乱数の種設定せず に、乱数を発生 させてきましたが、それにも関わらず、ランダムな AI である ai2毎回異なる合法手選択 するのは おかしい思った人いないでしょうか

乱数の種設定しない 場合は、プログラム実行した時間整数変換した値 を使って、random モジュールインポート した 時点乱数の種自動的に設定 されます。そのため、異なる時間実行した場合 は、異なる乱数の種設定される ため、プログラム実行するたび毎回異なるパターン乱数が発生する という 仕組み になっています。

従って、乱数計算再現性求めない場合 は、乱数の種設定 しては いけません

上記からわかるように、乱数利用するプログラム は、プログラム実行した時間全く同じ であれば、同じパターン乱数を発生 するので、再現性のある プログラムです。ただし、全く同じ時間プログラムを実行 することは 現実的 にはほぼ 不可能 なので、実質的 には 再現性のない プログラムと 考えて良い でしょう。

乱数の種を使った AI どうしの対戦の再現

下記 のプログラムは、ai12s ver 2 VS ai2対戦 を、乱数の種設定せず3 回 行い、その後で 乱数の種0 を設定 して 3 回行う プログラムです。

mb = Marubatsu()

for i in range(3):
    mb.play(ai=[ai2, ai2])

for i in range(3):
    random.seed(0)
    mb.play(ai=[ai2, ai2])
実行結果(長いのでクリックして開いてください)
Turn o
...
...
...

Turn x
...
O..
...

Turn o
..X
o..
...

Turn x
..x
oO.
...

Turn o
.Xx
oo.
...

Turn x
Oxx
oo.
...

Turn o
oxx
oo.
.X.

winner o
oxx
oo.
.xO

Turn o
...
...
...

Turn x
..O
...
...

Turn o
..o
..X
...

Turn x
O.o
..x
...

Turn o
o.o
..x
..X

winner o
oOo
..x
..x

Turn o
...
...
...

Turn x
...
..O
...

Turn o
...
..o
..X

Turn x
...
.Oo
..x

Turn o
X..
.oo
..x

winner o
x..
Ooo
..x

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo

実行結果 から、最初3 回対戦試合経過毎回異なります が、その後の 3 回対戦 は、全く同じ試合経過対戦 が行われることが 確認 できます。

従って、乱数の種 によって ランダムな AI対戦再現できる ことが 確認 できました。

play メソッドの修正

ランダムな AI対戦再現する際 に、random.seed呼び出してからplay メソッドを 呼び出す のは 面倒 なので、play メソッドの 仮引数乱数の種代入 するように 修正 し、実際の 乱数の種設定 は、play メソッドの 中で行う ようにします。

そのようにする 場合は、仮引数下記 のように 設定 するのが 一般的 です。仮引数デフォルト引数 にすることで、その 仮引数対応する実引数記述するか どうかで、関数行う処理再現性有無選択 することが できる ように なります

  • 仮引数名前(seed)を表す seed とする
  • 仮引数 を、デフォルト値Noneデフォルト引数 にする
  • 仮引数代入 された None 以外 の場合に、その値 を使って 乱数の種設定 し、None の場合何もしない

下記は、乱数の種設定できる ように、play メソッドを 修正 したプログラムです。

  • 1 行目デフォルト値None設定 した デフォルト引数 seed追加 する
  • 3、4 行目seedNone でない 場合は、seed乱数の種 として 設定 する
1  def play(self, ai, params=[{}, {}], verbose=True, seed=None):
2      # seed が None でない場合は、seed を乱数の種として設定する
3      if seed is not None:
4          random.seed(seed)
元と同じなので略
5
6  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, params=[{}, {}], verbose=True, seed=None):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        if verbose:
            print(self)
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        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("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        print(self)
    return self.status

Marubatsu.play = play
修正箇所
def play(self, ai, params=[{}, {}], verbose=True, seed=None):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
元と同じなので略

Marubatsu.play = play

下記 のプログラムは、先程と同様ai12s ver 2 VS ai2対戦 を、乱数の種設定せず1 回 行い、その後乱数の種0 を設定 して 2 回行う プログラムです。

mb = Marubatsu()

for i in range(3):
    mb.play(ai=[ai2, ai2])

for i in range(3):
    mb.play(ai=[ai2, ai2], seed=0)
修正箇所
mb = Marubatsu()

for i in range(3):
    mb.play(ai=[ai2, ai2])

for i in range(3):
-   random.seed(0)
-   mb.play(ai=[ai2, ai2])
+   mb.play(ai=[ai2, ai2], seed=0)

実行結果(長いのでクリックして開いてください)
Turn o
...
...
...

Turn x
...
...
.O.

Turn o
...
..X
.o.

Turn x
...
.Ox
.o.

Turn o
.X.
.ox
.o.

Turn x
.x.
.ox
.oO

Turn o
.xX
.ox
.oo

Turn x
.xx
Oox
.oo

winner x
Xxx
oox
.oo

Turn o
...
...
...

Turn x
.O.
...
...

Turn o
.o.
..X
...

Turn x
.o.
..x
O..

Turn o
.o.
..x
o.X

Turn x
.o.
..x
oOx

winner x
.oX
..x
oox

Turn o
...
...
...

Turn x
...
.O.
...

Turn o
.X.
.o.
...

Turn x
.x.
.o.
.O.

Turn o
Xx.
.o.
.o.

Turn x
xx.
.oO
.o.

Turn o
xx.
.oo
.oX

Turn x
xx.
.oo
Oox

winner x
xxX
.oo
oox

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo

先程と同様 に、最初3 回試合経過毎回異なります が、その後3 回試合経過毎回必ず同じに なることが 確認 できます。

ai12s ver 2 VS ai2ai12s ver 3 VS ai2 の違いの表示

先程紹介した、ランダムな AI である ai2 と対戦 した際に、ai12s ver 2対戦 すると 勝利 するが、ai12s ver 3対戦 すると 引き分け になる AI を見つける 下記アルゴリズム は、乱数の種 を使って 記述 することができます。どのようなプログラムを記述すれば良いかについて少し考えてみて下さい。

  1. 世の中に存在 する AI の中 から ランダムAI一つ選択 する
  2. ai12s ver 2 VS その AI対戦 を行い 結果を記録 する
  3. ai12s ver 3 VS その AI対戦 を行い 結果を記録 する
  4. 手順 23対戦結果同じ場合手順 1戻る
  5. 対戦結果異なる場合 は、その AI求める AI である

上記の 手順 1 ~ 3 は、下記 のような方法で 処理を行う ことができます。

手順 処理の方法
手順 1 これまで選択していない乱数の種1 つ選択する
手順 2、3 対戦行う前 に、手順 1選択 した 乱数の種設定する

手順 1選択 する 最初乱数の種何でも構わない ので、本記事 では 0 を選択 することにします。また、手順 1その後選択 する 乱数の種 には、直前選択 した 乱数の種1 を足した値選択 するという方法が 最も簡単 でしょう。1 を足す ことで、これまでに選択した どの乱数の種より大きな値になる ので、これまでに選択 した 乱数の種 とは、必ず異なる値選択 することが できます

下記は 上記の処理 を行うプログラムです。

  • 1 行目乱数の種代入 する変数 seed0 で初期化 する(手順 1
  • 3 ~ 11 行目答え見つかる まで 無限に繰り返す
  • 4 行目seed代入 された 乱数の種 で 、ai12s ver 2 VS ai2対戦対戦経過表示せず に(verbose=False)行い、結果winner1代入 する(手順 2
  • 5、6 行目seed代入 された 乱数の種 で 、ai12s ver 3 VS ai2対戦対戦経過表示せず に(verbose=False)行い、結果winner2代入 する(手順 3
  • 7 ~ 10 行目:それぞれの 対戦結果異なる場合 に、その乱数の種ai12s ver 2 VS ai2ai12s ver 3 VS ai2対戦経過表示 し、無限ループ から 抜ける手順 5
  • 11 行目乱数の種1 を足す手順 1)。ここで while 文ブロックが終了 しているので、4 行目戻り次の繰り返し処理行われる手順 4
 1  seed = 0
 2  mb = Marubatsu()
 3  while True:
 4      winner1 = mb.play(ai=[ai12s, ai2], seed=seed, verbose=False)
 5      winner2 = mb.play(ai=[ai12s, ai2], params=[{"score_victory": 200}, {}], \
 6                        seed=seed, verbose=False)
 7      if winner1 != winner2:
 8          mb.play(ai=[ai12s, ai2], seed=seed)
 9          mb.play(ai=[ai12s, ai2], params=[{"score_victory": 200}, {}], seed=seed)
10          break
11      seed += 1
行番号のないプログラム
seed = 0
mb = Marubatsu()
while True:
    winner1 = mb.play(ai=[ai12s, ai2], seed=seed, verbose=False)
    winner2 = mb.play(ai=[ai12s, ai2], params=[{"score_victory": 200}, {}], \
                      seed=seed, verbose=False)
    if winner1 != winner2:
        mb.play(ai=[ai12s, ai2], seed=seed)
        mb.play(ai=[ai12s, ai2], params=[{"score_victory": 200}, {}], seed=seed)
        break
    seed += 1

実行結果

Turn o
...
...
...

Turn x
...
.O.
...

Turn o
...
.oX
...

Turn x
..O
.ox
...

Turn o
..o
.ox
..X

winner o
..o
.ox
O.x

Turn o
...
...
...

Turn x
...
.O.
...

Turn o
...
.oX
...

Turn x
..O
.ox
...

Turn o
..o
.ox
..X

Turn x
..o
.ox
.Ox

Turn o
..o
.ox
Xox

Turn x
O.o
.ox
xox

Turn o
oXo
.ox
xox

winner draw
oxo
Oox
xox

下図 は、上記プログラム発見 した、結果異なる ai12s ver 2 VS ai2ai12s ver 3 VS ai2試合経過上下に並べた ものです。 から、4 手目まで試合経過同じ であり、5 手目ai11s ver 2ver 3選択 する 合法手異なったため試合の結果異なる ように なった ことが わかりました

従って、5 手目ai12s ver 3行う着手検証すればよい ことが わかりました

記事が長くなったので、その検証は次回の記事で行うことにします。

今回の記事のまとめ

今回の記事では、ルール条件の統合 について 説明 し、実際に ルール 12条件の 1 つ統合 することが できる ことを 示しました

また、必勝の局面 での 最善手見つける ための、異なる条件評価値同じ にした ai12 ver 3ai2対戦 を行った結果、バグが発生 しました。その バグの原因見つけるため に、プログラム再現性 と、乱数の種 について 説明 し、問題発生 した 可能性高い局面見つけました

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

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

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

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

次回の記事

  1. 本記事では説明は省略しますが、引き分けの局面 でも 同様 です

  2. 一度決めた後 は、常に同じ値使い続ける値 の事です

1
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
1
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?