LoginSignup
0
0

Pythonで〇×ゲームのAIを一から作成する その56 棋譜の記録と、問題のある試合経過の検証

Last updated at Posted at 2024-02-22

目次と前回の記事

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

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

これまでに作成した 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」が 最も少ない 着手を行う
そうでない場合は ランダム なマスに 着手 する

基準となる 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 98.1 0.0 1.9 82.5 1.9 15.6 90.3 1.0 8.7 あり

ai11s の問題点

前回の記事では、ルール 10 に、相手不利になる ように『「自 0 敵 1 空 2」が 最も少ない 着手を行う』という 条件を追加 した ルール 11定義 しました。しかし、その ルール 11実装 した ai11s と、ルール 10実装 した ai10s を下記のプログラムで 対戦 させると、実行結果の 通算成績 から ai10s 対して 弱くなる ことが分かりました。今回の記事では、そのようなことが起きる 原因調べ方 について説明します。

from ai import ai_match, ai10s, ai11s

ai_match(ai=[ai11s, ai10s])

実行結果

ai11s VS ai10s
count     win    lose    draw
o        2206       0    7794
x           0    4968    5032
total    2206    4968   12826

ratio     win    lose    draw
o       22.1%    0.0%   77.9%
x        0.0%   49.7%   50.3%
total   11.0%   24.8%   64.1%

AI が弱くなった場合の対処方法

これまでは、作成した AI強さの確認 は、ai_match という関数で 別の AI複数回対戦 し、その 通算成績見る ことで行ってきました。また、その 結果想定した結果異なる 場合は、プログラムの欠陥探して修正する という 方法対処 してきました。

前回の記事で説明したように、必要条件十分条件性質 を満たす 条件 であれば、ルール組み込む ことで、少なくとも AI弱くならない ことが 保証される ので、そのような条件新しく組み込んだ AI が 弱くなった場合 は、プログラムの実装間違っている ことを 疑う という 対処法有効な場合多いでしょう。しかし、必要条件 でも 十分条件 でも ない 条件は、ルールに加えた 場合に、AI弱くなる可能性 があるので、AI弱くなった場合 は、条件そのもの欠陥疑う必要 があります。

問題の原因の絞り込み

ルール 11条件 の中で、必要条件 でも 十分条件 でも ない ものは 以下の通り です。

  • 真ん中 のマスに 優先的着手 する
  • 以下3 つ の条件を 総合的判断 して着手を行う
    • 自分の手番勝利できる ように「自 2 敵 0 空 1」が 1 つ存在する 着手を行う
    • 自分有利になる ように「自 1 敵 0 空 2」が 最も多い 着手を行う
    • 相手不利になる ように「自 0 敵 1 空 2」が 最も少ない 着手を行う

この中で、「真ん中 のマスに 優先的着手 する」という条件は、初期のルール追加 しており、実際 にこの条件を ルールに組み込む ことによって AI強くなった ことが 確認できている ので、問題 がある 可能性低そう です。そこで、この条件検証後回し にし、残りの条件問題がない ことが 分かった場合改めて調べる ことにします。

3 つ条件総合的判断 して 着手を行う という 条件 に対する 評価値 は、それぞれの条件 に対して、下記の表 のように 設定 した 評価値の合計計算 していますが、この 評価値計算方法 が、強い AI のための 条件 として ふさわしくない可能性 があります。

局面の状況 評価値
「自 2 敵 0 空 1」が 1 つ存在する 1
「自 1 敵 0 空 2」が x 個存在する x
「自 0 敵 1 空 2」が y 個存在する -y

問題が発生している可能性が高い試合の観察

上記評価値計算方法問題 がある 可能性が高い ことが分かりましたが、この表を眺めているだけでは、どこに問題がある かを 知る ことは 困難 でしょう。その理由は、〇×ゲーム勝利するため に、『「自 2 敵 0 空 1」が 1 つ存在する』、『「自 1 敵 0 空 2」が x 個存在する』、『「自 0 敵 1 空 2」が y 個存在する』の 3 つの条件 の、それぞれの 重要度どれくらいであるか がはっきりと わからない からです。

はっきりとわからない理由 は、必要条件 でも、十分条件 でも ないから です。

そのような場合は、実装した AI行った対戦 の中で、問題が発生 している 可能性が高い試合行われた着手観察 することで 問題の原因探る という 方法 があります。

下記 は、先程の ai11s VS ai10s対戦結果再掲 したものです。よく見ると、ai11s〇 を担当 した場合は、勝率約 20 %敗率0 % となっているので、問題はなさそう です。一方、× を担当 した場合は、勝率0 %敗率50 % となっており、何か 問題が発生 している 可能性が高い ことが 分かります。このことから、ai11s VS ai10s試合 で、ai11s× を担当 して 負けた試合観察 すれば良いことが分かります。

ai11s VS ai10s
count     win    lose    draw
o        2206       0    7794
x           0    4968    5032
total    2206    4968   12826

ratio     win    lose    draw
o       22.1%    0.0%   77.9%
x        0.0%   49.7%   50.3%
total   11.0%   24.8%   64.1%

ai11s VS ai10s で、ai11s× を担当 して 負ける試合 は、mb.play(ai=[ai10s, ai11s])実行 し、返り値ai11s の敗北 を表す Marubatsu.CIRCLEなった場合 です。

そのような試合は、下記のプログラムのように、while 文 を使って ai11s負けるまでmb.play(ai=[ai10s, ai11s])繰り返し呼び出す という 処理を行う ことで 観察 することが できます最後表示された試合 が、ai11s負けた試合 です。

今回の記事 では 以後 は、ai11s VS ai10s のように 記述 した場合は、先に記述 した ai11s〇を担当 して ai10s対戦を行う ことを表すことにします。

  • 4 行目while True による 無限ループ記述 する
  • 5、6 行目ai10s VS ai11s対戦 を行い、対戦経過表示する。また、その返り値Marubatsu.CIRCLE だった場合は、ai11s負けている ので、break 文実行 して 無限ループ から 抜ける
1  from marubatsu import Marubatsu
2
3  mb = Marubatsu()
4  while True:
5     if mb.play([ai10s, ai11s]) == Marubatsu.CIRCLE:
6          break
行番号のないプログラム
from marubatsu import Marubatsu

mb = Marubatsu()
while True:
    if mb.play([ai10s, ai11s]) == Marubatsu.CIRCLE:
        break

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

Turn o
...
...
...

Turn x
...
.O.
...

Turn o
...
.o.
..X

Turn x
O..
.o.
..x

Turn o
o.X
.o.
..x

Turn x
o.x
.oO
..x

Turn o
o.x
Xoo
..x

Turn x
o.x
xoo
O.x

Turn o
oXx
xoo
o.x

winner draw
oxx
xoo
oOx

Turn o
...
...
...

Turn x
...
.O.
...

Turn o
..X
.o.
...

Turn x
..x
.o.
O..

Turn o
..x
Xo.
o..

Turn x
..x
xo.
o.O

Turn o
.Xx
xo.
o.o

winner o
.xx
xo.
oOo

このプログラムで、絶対に勝利しない ような AI どうし対戦を行う と、無限ループ から 抜けられない ので、処理終わらなくなる 点に 注意 して下さい。

上記のプログラムの問題点

上記 のプログラムを 何度か実行 すれば わかる と思いますが、上記のプログラムには、ai11s負けるまで 何度も 対戦を行う ので、運が悪いai11s負けていない試合延々と表示 されてしまいます。実際 に、上記実行結果 では、最初引き分け の試合が 表示 され、その次ai11s負ける試合表示 されます。表示 する 必要がある のは、ai11s負けた試合だけ なので、余計な表示 が行われるのは 望ましくありません

上記の場合は、ai11s負ける可能性50 % もある ので 運が悪くない限り余計な試合数多く表示 されることは ありません が、ai11s負ける可能性低い場合 は、余計な試合 がものすごく 多く表示されてしまう ことになります。

そこで、上記の プログラム修正 し、ai11s負けた試合だけ表示 するようにします。どのように修正すれば良いかを少し考えてみて下さい。

棋譜の必要性

余計な試合表示しない ようにするために、下記のプログラムの 3 行目 のように、verbose=False記述 して play メソッドを 呼び出す という方法を 思いついた人いるかもしれません が、この方法 では、ai11s負けた試合表示されなく なります。

mb = Marubatsu()
while True:
    if mb.play([ai10s, ai11s], verbose=False) == Marubatsu.CIRCLE:
        break
修正箇所
mb = Marubatsu()
while True:
-   if mb.play([ai10s, ai11s]) == Marubatsu.CIRCLE:
+   if mb.play([ai10s, ai11s], verbose=False) == Marubatsu.CIRCLE:
        break

実行結果

考え方間違っていない のですが、残念ながら、下記 のような 理由 で、verbose=False だけ では、ai11s負けた時だけ 試合経過を 表示 することは できません

  • verbose=False は、ゲーム開始する前指定 する 必要 がある
  • ai11s負けるか どうかは、ゲームが終了 するまで わからない

この 問題を解決 する 方法の一つ は、ゲームを 行う際 に、行われた着手記録しておく という方法です。一般的 に、ゲームプレイヤー が行った 行動の記録 の事を 棋譜 と呼ぶので、本記事でも 棋譜 という 用語を使う ことにします。

棋譜記録しておく ことで、ゲームの終了後 に、新しいゲーム開始 し、その 棋譜通り着手を行う ことで、試合経過再現 することが できます

〇×ゲーム棋譜どのように記録 すればよいかについて少し考えてみて下さい。

棋譜の記録の実装

まず、棋譜 をどのような データ構造 で表現するかを 決める必要 があります。棋譜 は、行われた着手順番に並べた ものなので、着手 を表す データ要素 として持つ list 使って 表現 することが できます

棋譜 は、〇×ゲーム関するデータ なので、Marubatsu クラスの インスタンス属性記録 することにします。棋譜英語record なので、records という 名前 にします。

棋譜 のような 記録したデータ を代入する 変数の名前 には、他にも history(履歴)や log(記録)などの 名前良く使われます

restart メソッドの修正

棋譜初期化 は、ゲーム開始時行う必要 があるので、restart メソッドの中に その処理 を下記のように 記述 します。

  • 8 行目records 属性を、空の list初期化 する
1   def restart(self):
2       self.initialize_board()
3       self.turn = Marubatsu.CIRCLE     
4       self.move_count = 0
5       self.status = Marubatsu.PLAYING
6       self.last_move = -1, -1          
7       self.last_turn = None
8       self.records = []
9 
10  Marubatsu.restart = restart
行番号のないプログラム
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    self.last_move = -1, -1          
    self.last_turn = None
    self.records = []

Marubatsu.restart = restart
修正箇所
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    self.last_move = -1, -1          
    self.last_turn = None
+   self.records = []

Marubatsu.restart = restart

move メソッドの修正

棋譜更新 は、着手の際行う必要 があるので、move メソッドの中に その処理 を下記のように 記述 します。

  • 8 行目records 属性の 要素 に、着手 した 座標 のデータを表す self.last_move追加 する。なお、この部分は、self.records.append((x, y))記述しても良い
 1  def move(self, x, y):
 2      if self.place_mark(x, y, self.turn):
 3          self.last_turn = self.turn
 4          self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
 5          self.move_count += 1
 6          self.status = self.judge()
 7          self.last_move = x, y
 8          self.records.append(self.last_move)
 9           
10  Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.last_turn = self.turn
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
        self.status = self.judge()
        self.last_move = x, y
        self.records.append(self.last_move)
            
Marubatsu.move = move
修正箇所
def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.last_turn = self.turn
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
        self.status = self.judge()
        self.last_move = x, y
+       self.records.append(self.last_move)
            
Marubatsu.move = move

棋譜の表示

上記のように修正することで、ai11s負けた時棋譜 を、下記のプログラムで 表示 することが できるように なります。

  • 3、4 行目ai11s負けた時に棋譜代入 された mb.records表示 する
1  mb = Marubatsu()
2  while True:
3     if mb.play([ai10s, ai11s], verbose=False) == Marubatsu.CIRCLE:
4          print(mb.records)
5          break
行番号のないプログラム
mb = Marubatsu()
while True:
    if mb.play([ai10s, ai11s], verbose=False) == Marubatsu.CIRCLE:
        print(mb.records)
        break
修正箇所
mb = Marubatsu()
while True:
    if mb.play([ai10s, ai11s], verbose=False) == Marubatsu.CIRCLE:
+       print(mb.records)
        break

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

[(1, 1), (0, 2), (2, 0), (2, 1), (1, 0), (2, 2), (1, 2)]

棋譜によるゲームの経過の表示

棋譜 には、行われた着手順番に記録 されているので、for 文move メソッドを 利用 することで、下記のプログラムのように、ai11s負けた時試合経過表示 することができます。実行結果 から、負けた時試合だけ表示 されることが 確認 できます。

  • 4 行目ai11s負けた試合棋譜records代入 する
  • 5 行目棋譜 を使って 負けた試合再現 するために、restart メソッドを呼び出して、ゲームリセット する。なお、この処理4 行目より前 に行っては いけない 点に 注意 する事。その理由は、restart メソッドによって 棋譜リセット されてしまうため、4 行目で records空の list代入 されてしまうことになるからである
  • 6 行目棋譜 から 順番着手 のデータを 取り出しxy代入 する
  • 7、8 行目(x, y) のマスに 着手 を行い、その 結果ゲーム盤表示 する
1  mb = Marubatsu()
2  while True:
3      if mb.play([ai10s, ai11s], verbose=False) == Marubatsu.CIRCLE:
4          records = mb.records
5          mb.restart()
6          for x, y in records:
7              mb.move(x, y)
8              print(mb)
9          break
行番号のないプログラム
mb = Marubatsu()
while True:
    if mb.play([ai10s, ai11s], verbose=False) == Marubatsu.CIRCLE:
        records = mb.records
        mb.restart()
        for x, y in records:
            mb.move(x, y)
            print(mb)
        break
修正箇所
mb = Marubatsu()
while True:
    if mb.play([ai10s, ai11s], verbose=False) == Marubatsu.CIRCLE:
+       records = mb.records
+       mb.restart()
+       for x, y in records:
+           mb.move(x, y)
+           print(mb)
        break

実行結果

Turn x
...
.O.
...

Turn o
X..
.o.
...

Turn x
x..
.o.
..O

Turn o
x..
.o.
.Xo

Turn x
x.O
.o.
.xo

Turn o
x.o
.o.
Xxo

winner o
x.o
.oO
xxo

指定した条件の試合経過を表示する関数の定義

特定AI どうし対戦 で、指定 した 結果になる 場合の 試合経過表示 する際に、毎回上記 のような プログラムを記述する のは 大変 なので、上記の処理 を行う、下記のような 関数定義 する事にします。

名前試合経過(progress)を 表示(show)するので show_progress とする
処理指定 した AI どうし の対戦で、指定 した 結果試合経過表示 する
入力仮引数 ai対戦 を行う AI を、winner試合結果代入 する
出力:なし

show_progress下記 のプログラムのように 定義 します。

  • 1 行目仮引数aiwinner記述 する
  • 2 ~ 10 行目4 行目以外元のプログラム同じ
  • 4 行目play メソッドの 実引数ai=ai記述 するように 修正 し、play メソッドの 返り値 と 仮引数 winner等しいか どうかを 判定 するように修正する
 1  def show_progress(ai, winner):
 2      mb = Marubatsu()
 3      while True:
 4          if mb.play(ai=ai, verbose=False) == winner:
 5              records = mb.records
 6              mb.restart()
 7              for x, y in records:
 8                  mb.move(x, y)
 9                  print(mb)
10              break
行番号のないプログラム
def show_progress(ai, winner):
    mb = Marubatsu()
    while True:
        if mb.play(ai=ai, verbose=False) == winner:
            records = mb.records
            mb.restart()
            for x, y in records:
                mb.move(x, y)
                print(mb)
            break
修正箇所
+def show_progress(ai, winner):
    mb = Marubatsu()
    while True:
-       if mb.play([ai10s, ai11s], verbose=False) == Marubatsu.CIRCLE:
+       if mb.play(ai=ai, verbose=False) == winner:
            records = mb.records
            mb.restart()
            for x, y in records:
                mb.move(x, y)
                print(mb)
            break

ai10s VS ai11s で、ai11s負ける試合 は、下記 のプログラムで 表示 できます。実行結果 から、ai11s負ける試合正しく表示 できていることが 確認 できます。

show_progress([ai10s, ai11s], Marubatsu.CIRCLE)

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

Turn x
...
.O.
...

Turn o
X..
.o.
...

Turn x
x..
.o.
..O

Turn o
x..
.o.
.Xo

Turn x
x.O
.o.
.xo

Turn o
x.o
.o.
Xxo

winner o
x.o
.oO
xxo

なお、定義 した show_progress は、ai.py記述する ことにします。

同一局面の定義と扱い

試合経過の観察を行う前に、同一局面 に関する 定義 と、その扱い について説明します。

同一局面の定義

〇×ゲームゲーム盤 は、3 x 3上下左右対象 となる 形状 をしています。また、将棋やチェスとは異なり、ゲーム盤向き存在しません1。そのようなゲーム盤は、ゲーム盤を 回転させたりする ことで、異なる局面別の局面一致する場合 があります。本記事では そのような局面 のことを 同一局面 と呼ぶことにします。

例えば、下図の 左の局面 を、時計回り90 度回転 させると、右の局面一致する ので、この 2 つ同一局面 です。

同一局面の種類

〇×ゲーム同一局面 は、以下のように 分類 できます。

回転による同一局面

ある局面を 時計回り90 度ずつ回転 した 局面同一局面 ですが、90 度回転4 回行う360 度 になって 元の局面もどる ので、回転 による 同一局面4 種類 ある事になります。それぞれの マス1 ~ 9 までの 番号割り当てた 場合、回転 による 同一局面下図1 行目 のようになります。なお、下図2 行目同一局面具体例 です。

線対称な同一局面

ある 直線 に対して、両側の形同じ図形 の事を 線対称図形 と呼びます。線対称図形 は、その 直線を軸 として、180 度ひっくり返す と、同じ形図形 になります。

〇×ゲーム の場合は、真ん中のマス通る垂直水平左上から右下方向右上から左下方向4 種類直線を軸 として、ゲーム盤表裏ひっくり返した局面同一局面 になります。下図 は、左上局面 に対する、4 種類線対称同一局面 です。

同一局面の一覧

上記から、〇×ゲーム では、ある局面同一局面 は、最大下図8 種類 です。

ただし、上記で「最大で」と 記述 したのは、8 種類 ある 同一局面いくつか が、完全に同じ局面場合がある からです。例えば、下図1 行目局面 に対する 同一局面4 種類2 行目局面 に対する 同一局面1 種類 しかありません。

同一局面の性質と本記事での扱い

〇×ゲームに限らず、ほとんどのゲーム では、同一局面 は、見た目は違う かもしれませんが、ゲームの勝敗考える上 では、同じ局面みなす ことが できます。そこで、本記事 では 同一局面 に対しては、その中の 1 つ局面だけ考慮 することで、残り同一局面 に対する 考慮行ったとみなす ことにします。

試合経過の観察と検証

これまでは、AI どうし試合勝敗の結果 しか 見てきませんでした が、試合経過観察 し、検証 を行うことで、AIそれぞれの局面 で、具体的どのように 着手を 選択 しているかが わかるようになります。また、その 検証を行う ことで、ルール条件問題点 や、その問題点の 解決法わかるようになる 場合があります2。そこで、上記の ai10s VS ai11s試合経過観察 し、a11s問題点検証 することにします。

わかりやすいように、先程の 試合経過表現 します。下図は、黄色のマス着手 を行ったマスを、上に表示 されているのはその 着手を行った AI を表します。

まず、ai10sai11sそれぞれの局面 に対して、どのような判断 に基づいて 着手を選択 したかについて 検証 することにします。

1 手目の検証

ルール 10最も優先順位高い条件 は、「真ん中 のマスに 優先的着手 する」なので、1 手目ai10必ず真ん中(1, 1) のマスに 着手行うはず です。上記の試合でも、実際そのような着手行われています

1 手目真ん中のマス着手行われた ので、以後ai10sai11s も、最も高い 評価値が 計算 される 合法手を選択 します。

2 手目の検証

2 手目ai11s(0, 0) のマスに 着手 していますが、この マス選択された理由 を、それぞれの 合法手 に着手した局面の 評価値どのように計算された かを、ルールに基づいて 実際に 調べる ことで 検証 することにします。

評価値の確認

ai11s が、それぞれの 合法手 に対して 計算 する 評価値 は、下記のプログラムのように、実引数debug=True記述 して ai11s呼び出す ことで 表示する ことが できます

  • 2 行目2 手目着手する前局面作る
  • 3 行目その局面 に対して、ai11s を、デバッグ表示を行う ように debug=True実引数に記述 して 呼び出す
mb = Marubatsu()
mb.move(1, 1)
ai11s(mb, debug=True)

実行結果

Start ai_by_score
Turn x
...
.O.
...

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

score -1 best score -inf
UPDATE
  best score -1
  best moves [(0, 0)]
====================
move (1, 0)
Turn o
.X.
.o.
...

score -2 best score -1
====================
move (2, 0)
Turn o
..X
.o.
...

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

score -2 best score -1
====================
move (2, 1)
Turn o
...
.oX
...

score -2 best score -1
====================
move (0, 2)
Turn o
...
.o.
X..

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

score -2 best score -1
====================
move (2, 2)
Turn o
...
.o.
..X

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

下記はそれぞれの 合法手着手した際評価値 です。

合法手 (0, 0) (1, 0) (2, 0) (0, 1) (2, 1) (0, 2) (1, 2) (2, 2)
評価値 -1 -2 -1 -2 -2 -1 -2 -1

評価値最大値-1 なので、評価値-1 である (0, 0)(2, 0)(0, 2)(2, 2) の中から ランダム着手選択される ことが分かります。そのことは、下記の 実行結果最後の 2 行 からも 確認 できます。

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

実際行われた着手 である (0, 0) は、その 4 つ候補の中入っている ので、(0, 0)着手された理由確認 できました。

同一局面をまとめる

2 手目合法手 は、真ん中のマス除いた 8 マス がありますが、隅のマス着手 した場合は、ゲーム盤回転 することで (0, 0) のマスに 着手 した場合と 同一局面 になります。同様 に、 のマスに 着手 した場合は、ゲーム盤回転 することで (1, 0) のマスに 着手 した場合と 同一局面 になります。下図はそのことを表した図です。

また、実際隅に着手 した場合の 評価値すべて -1 に、辺に着手 した場合の 評価値すべて -2 になっていることから、同一局面 に対する 評価値同じになる ことが 確認 できます。従って、上記の表 を、下記 のように まとめる ことが できます

合法手 評価値
隅の 4 マス -1
辺の 4 マス -2

デバッグ表示にマークのパターンの数を表示する修正

ai11s(mb, debug=True)実行結果表示 には、それぞれの 合法手 を着手した局面の 評価値表示されます が、その評価値が どのように計算されたか表示されません。そこで、下記のプログラムのように、ai11s評価値計算する際利用 する、マークのパターン表示 するように ai11s を修正 することにします。

  • 8、9 行目仮引数 debugTrue の場合に、count_markpats計算 した、マークのパターン表示 するように 修正 する
 1  from pprint import pprint
 2  from ai import ai_by_score
 3  from marubatsu import Markpat
 4
 5  def ai11s(mb, debug=False):
 6     def eval_func(mb):      
元と同じなので省略
 7         markpats = mb.count_markpats()
 8         if debug:
 9             pprint(markpats)
10          # 相手が勝利できる場合は評価値として -100 を返す
11          if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
12              return -100
元と同じなので省略
行番号のないプログラム
from pprint import pprint
from ai import ai_by_score
from marubatsu import Markpat

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

        markpats = mb.count_markpats()
        if debug:
            pprint(markpats)
        # 相手が勝利できる場合は評価値として -100 を返す
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
            return -100
        # 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
            return 100

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

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

def ai11s(mb, debug=False):
    def eval_func(mb):      
元と同じなので省略
        markpats = mb.count_markpats()
+       if debug:
+           pprint(markpats)
        # 相手が勝利できる場合は評価値として -100 を返す
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
            return -100
元と同じなので省略

上記の修正 を行うことで、先程と同じ 下記のプログラムを 実行 すると、評価値表示する直前 に、マークのパターンの数表示 されるようになります。

mb = Marubatsu()
mb.move(1, 1)
ai11s(mb, debug=True)

実行結果

略
move (0, 0)
Turn o
X..
.o.
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 2,
             Markpat(last_turn=0, turn=1, empty=2): 3,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -1 best score -inf
略
実行結果の全体(長いのでクリックして表示して下さい)
Start ai_by_score
Turn x
...
.O.
...

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

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

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): 1,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -2 best score -1
====================
move (2, 0)
Turn o
..X
.o.
...

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

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): 1,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -2 best score -1
====================
move (2, 1)
Turn o
...
.oX
...

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): 1,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -2 best score -1
====================
move (0, 2)
Turn o
...
.o.
X..

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

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

ai9sai10s も下記のプログラムのように、同様の方法修正 することにします。

ai9s(長いのでクリックして表示して下さい)
def ai9s(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 4 を返す
        if mb.last_move == (1, 1):
            return 4
    
        # 自分が勝利している場合は、評価値として 3 を返す
        if mb.status == mb.last_turn:
            return 3

        markpats = mb.count_markpats()
        if debug:
            pprint(markpats)
        # 相手が勝利できる場合は評価値として -1 を返す
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
            return -1
        # 次の自分の手番で自分が必ず勝利できる場合は評価値として 2 を返す
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
            return 2
        # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
            return 1
        # それ以外の場合は評価値として 0 を返す
        else:
            return 0

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

        markpats = mb.count_markpats()
+       if debug:
+           pprint(markpats)
        # 相手が勝利できる場合は評価値として -1 を返す
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
            return -1
        # 次の自分の手番で自分が必ず勝利できる場合は評価値として 2 を返す
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
            return 2
        # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
            return 1
        # それ以外の場合は評価値として 0 を返す
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug) 
ai10s(長いのでクリックして表示して下さい)
def ai10s(mb, debug=False):
    def eval_func(mb):      
        # 真ん中のマスに着手している場合は、評価値として 300 を返す
        if mb.last_move == (1, 1):
            return 300
    
        # 自分が勝利している場合は、評価値として 200 を返す
        if mb.status == mb.last_turn:
            return 200

        markpats = mb.count_markpats()
        if debug:
            pprint(markpats)
        # 相手が勝利できる場合は評価値として -100 を返す
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
            return -100
        # 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
            return 100

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

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

        markpats = mb.count_markpats()
+       if debug:
+           pprint(markpats)
        # 相手が勝利できる場合は評価値として -100 を返す
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
            return -100
        # 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
            return 100

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

    return ai_by_score(mb, eval_func, debug=debug)

ローカル関数のブロックの中の名前解決

下記は 修正 した ai11s の一部を再掲したプログラムです。先程は、下記のプログラムの 12 行目debug が、1 行目ai11s仮引数 debug同じものである として 説明 を行いましたが、12 行目debug は、グローバル関数 である ai11sブロックの中記述 されてはいますが、同時ローカル関数 である eval_funcブロックの中 にも 記述 されています。そこで、ローカル関数の中 で行われる 名前解決 について説明します。

 1  def ai11s(mb, debug=False):
 2      def eval_func(mb):      
 3          # 真ん中のマスに着手している場合は、評価値として 300 を返す
 4          if mb.last_move == (1, 1):
 5              return 300
 6     
 7          # 自分が勝利している場合は、評価値として 200 を返す
 8          if mb.status == mb.last_turn:
 9              return 200
10
11          markpats = mb.count_markpats()
12          if debug:
13              pprint(markpats)

下図は、ai11sグローバルスコープローカルスコープ図示 したものです。図のように、グローバル関数 である ai11 の中定義 された ローカル関数 eval_func に対しても ローカルスコープ が作られ、3 種類スコープ入れ子構造 になります。

上図の、水色の ローカル関数ローカルスコープ内 での 名前解決手順 は、難しそうに思えるかも しれませんが、実際 には以前の記事で説明した 手順と同じ です。

ローカル関数ブロックの中代入処理 が行われた場合は、以前の記事で説明した場合と 同様 に、その関数ローカル名前空間管理 する 名前値が代入 されます。

ローカル関数ブロックの中変数の値参照 された場合は、以前の記事で説明した 下記の手順同じ手順名前解決 が行われます。

  1. 名前が記述されている 式をスコープ とする 入れ子 になった 名前空間 のうち、最も内側にある 名前空間を 選択 する
  2. 選択した名前空間の中から 名前を探す
  3. 名前が 見つかった 場合は、その名前空間を使って、名前からオブジェクトを 対応づける
  4. 名前が 見つからなかった 場合は、一つ外側 の名前空間を 選択 して、手順 2 へ戻る
  5. 手順 4 で、外側の名前空間が 存在しない場合 は、NameError という エラーが発生 する

この手順 に従って、下記の 12 行目debug名前解決 は、下記の手順 で行われます。

  1. 12 行目debug をスコープとする 最も内側名前空間 は、ローカル関数 eval_func名前空間 である
  2. 12 行目debug は、ローカル関数 eval_func名前空間登録されていない
  3. 1 つ外側名前空間 は、グローバル関数 ai11s が管理する 名前空間 である
  4. ai11s仮引数 debugai11s名前空間登録されている ので、名前がみつかる
  5. ai11sローカル変数 debug を使って、12 行目処理行われる
 1  def ai11s(mb, debug=False):
 2      def eval_func(mb):      
 3          # 真ん中のマスに着手している場合は、評価値として 300 を返す
 4          if mb.last_move == (1, 1):
 5              return 300
 6     
 7          # 自分が勝利している場合は、評価値として 200 を返す
 8          if mb.status == mb.last_turn:
 9              return 200
10
11          markpats = mb.count_markpats()
12          if debug:
13              pprint(markpats)

グローバル変数ローカル変数同じ名前 であっても 異なる変数 であるように、「グローバル関数ローカル変数」と、そのグローバル関数の中で定義された「ローカル関数ローカル変数」は、同じ名前 であっても 異なる変数 である。

例えば、上記のプログラムの場合、ai11sローカル変数 mb と、eval_funcローカル変数 mb同じ名前 であっても、異なる変数 です。

グローバル関数ローカル変数」と、そのグローバル関数の中で定義された「ローカル関数ローカル変数」の関係は、グローバル変数ローカル変数関係似ています

global と nonlocal

globalnonlocal を使うと バグが発生しやすくなる ので、特別な理由 がない限り、あまり 使わないほうが良い でしょう。本記事 でも nonlocal利用 する 予定はない ので、この部分は 興味がない方読み飛ばして構いません

以前の記事で、関数のブロック の中で、global 変数名 を記述することで、その変数グローバル変数みなされるようになる ことを説明しました。

ローカル関数 の場合は、その 外側グローバル名前空間グローバル関数のローカル名前空間 があるので、global 変数名 だけでは、そのうちの グローバル名前空間 にしか 対応できません。そのため、Python では、nonlocal 変数名記述 することで、その 変数名一つ外側名前空間に登録 された 名前みなす ことができるようになっています。

下記は globalnonlocal を利用した です。処理の内容 は、コメント を見て下さい。

a = 1 # グローバル変数 a に 1 を代入する
b = 2 # グローバル変数 b に 2 を代入する

def x():
    def y():
        global b                 # b をグローバル変数とみなす
        print("global b =", b)   # グローバル変数 b を表示する

    def z():
        nonlocal b               # b を 1 つ外側の名前空間である x のローカル変数とみなす
        print("nonlocal b =", b) # x のローカル変数 b を表示する

    global a                # a をグローバル変数とみなす
    print("global a =", a)  # グローバル変数 a を表示する
    b = 3                   # 関数 x のローカル変数 b に 3 を代入する
    y()                     # ローカル関数 y を呼び出す
    z()                     # ローカル関数 z を呼び出す
      
x() # 関数 x を呼び出す

実行結果

global a = 1
global b = 2
nonlocal b = 3

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

評価値の計算方法の検証

下記は、先程実行した ai11s(mb, debug=True)実行結果 に、それぞれの 合法手着手 を行った 局面マークのパターン と、評価値表にまとめた ものです。

表のそれぞれの意味は以下の通りです。なお、以後は、合法手 を表の 左の列の番号 を使って、合法手 1 のように 表記 することにします。

  • 1 行目 では、マークのパターン自 x 敵 y 空 z」を「xyz」のように表記する
  • 2 行目 は、それぞれの マークのパターン に対する 評価値 を表す
  • 3 行目以降ゲーム盤の図 では、着手 を行った 合法手マス水色表示 する。また、同一局面 のうちの 1 つのみ表示 する
「201」 「021」 「102」 「012」 評価値
評価値 1:+1
2~:100
-100 1 つで
+1
1 つで
-1
1 2 3 -1
2 1 3 -2

表から、どの合法手着手 しても、相手が有利 になる「自 0 敵 1 空 2」の 同じ ですが、自分が有利 になる「自 1 敵 0 空 2」の は、隅に着手 したほうが 多くなり、その結果、着手 したほうが 評価値高くなる ことが分かりました。そのことは、下記の 実行結果最後の 2 行 からも 確認 できます。従って、この局面 では、ai11s100 % 隅のマス着手 を行います。また、実際に ai11s隅のマス着手行っています

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

このように、ai11s具体的どのように 評価値を 計算しているか検証 することで、ai11s選択した着手根拠明確 になります。また、その結果、AI が 意図通り の処理を行うことを 確認 できたり、不具合の原因発見 したりすることができる場合があります。実際に、上記 の場合は、意図通り の処理を 行っていること確認 できます。

3 手目の検証

本記事目的 は、ai11s選択 する 着手検証 ですが、ai10sルール は、ai11sルールかなり似ている ので、ai10s選択 する 着手検証 することにします。

2 手目検証同様 に、下記のプログラムで ai10s がそれぞれの 合法手 に対してどのような 評価値計算 するかを 表示 し、その 結果表にまとめる ことにします。

  • 1、2 行目2 手目(0, 0)着手 を行い、ai10s呼び出す
mb.move(0, 0)
ai10s(mb, debug=True)
実行結果(長いのでクリックして開いてください)
Start ai_by_score
Turn o
X..
.o.
...

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

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

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 1,
             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): 2,
             Markpat(last_turn=2, turn=0, empty=1): 1})
score 4 best score 3
UPDATE
  best score 4
  best moves [(2, 0)]
====================
move (0, 1)
Turn x
x..
Oo.
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 2,
             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): 2,
             Markpat(last_turn=2, turn=0, empty=1): 1})
score 3 best score 4
====================
move (2, 1)
Turn x
x..
.oO
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 1,
             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,
             Markpat(last_turn=2, turn=0, empty=1): 1})
score 4 best score 4
APPEND
  best moves [(2, 0), (2, 1)]
====================
move (0, 2)
Turn x
x..
.o.
O..

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 1,
             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): 2,
             Markpat(last_turn=2, turn=0, empty=1): 1})
score 4 best score 4
APPEND
  best moves [(2, 0), (2, 1), (0, 2)]
====================
move (1, 2)
Turn x
x..
.o.
.O.

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 1,
             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,
             Markpat(last_turn=2, turn=0, empty=1): 1})
score 4 best score 4
APPEND
  best moves [(2, 0), (2, 1), (0, 2), (1, 2)]
====================
move (2, 2)
Turn x
x..
.o.
..O

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

下記は、上記の結果まとめた表 です。なお、ルール 10 は、「自 0 敵 2 空 1」に関する 条件ありません ので、先程の表と異なり、「012」の 存在しません。また、先程は、同一局面を 1 つにまとめると説明しましたが、同一局面2 つの場合 は、両方表記 したほうが 分かりやすい 気がしましたので、下図では 同一局面横に並べて表記 します。

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

表から、ai10s実際着手 を行った、合法手 4(2, 2)着手 した場合の 評価値5最も高くなる ことが 確認 できました。そのことは、下記の 実行結果最後の 2 行 からも 確認 できます。従って、この局面 では、ai10s100 % (2, 2)着手 を行います。

best score 5
best moves [(2, 2)]

合法手 4 には「自 2 敵 0 空 1」は 存在しません が、それが 1 つ存在 する 合法手 2、3比較 して、自分が有利になる「自 1 敵 0 空 2」が 2 つ多い ので、合法手 4選択 することは、ルール 10意図通り言える でしょう。

4 手目の検証

これまで同様 に、下記のプログラムで ai11s がそれぞれの 合法手 に対して どのように 評価値を 計算 するかを 表示 し、その 結果表にまとめます

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

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

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

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

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

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

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

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

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

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

表から、合法手 23評価値-1最も高い ことが分かります。そのことは、下記の 実行結果最後の 2 行 からも 確認 できます。

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

合法手 23着手 はそれぞれ 2 通りずつ あるので、合法手 3合法手 4着手 が行われる 確率 は、それぞれ 2 / 4 = 50 % であることが分かります。従って、この局面 では、50 % の確率で、合法手 2 または 合法手 3着手行われる ことが分かります。実際の対戦ai11s合法手 3(1, 2)着手を行った のは、50 % の確率行われた からです。

また、合法手 23評価値同じ になる 理由 は、「自 2 敵 0 空 1」と「自 1 敵 0 空 2」の 合計等しい からであることが 確認 できました。これは、ルール 11意図通り計算 です。

5 手目の検証

これまで同様 に、下記のプログラムで ai10s がそれぞれの 合法手 に対して どのように 評価値を 計算 するかを 表示 し、その 結果表にまとめます

mb.move(1, 2)
ai10s(mb, debug=True)
実行結果(長いのでクリックして開いてください)
Start ai_by_score
Turn o
x..
.o.
.Xo

legal_moves [(1, 0), (2, 0), (0, 1), (2, 1), (0, 2)]
====================
move (1, 0)
Turn x
xO.
.o.
.xo

defaultdict(<class 'int'>,
            {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): 2,
             Markpat(last_turn=2, turn=1, empty=0): 2})
score 3 best score -inf
UPDATE
  best score 3
  best moves [(1, 0)]
====================
move (2, 0)
Turn x
x.O
.o.
.xo

defaultdict(<class 'int'>,
            {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): 3,
             Markpat(last_turn=2, turn=0, empty=1): 2,
             Markpat(last_turn=2, turn=1, empty=0): 1})
score 100 best score 3
UPDATE
  best score 100
  best moves [(2, 0)]
====================
move (0, 1)
Turn x
x..
Oo.
.xo

defaultdict(<class 'int'>,
            {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): 3,
             Markpat(last_turn=2, turn=0, empty=1): 1,
             Markpat(last_turn=2, turn=1, empty=0): 1})
score 3 best score 100
====================
move (2, 1)
Turn x
x..
.oO
.xo

defaultdict(<class 'int'>,
            {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): 2,
             Markpat(last_turn=2, turn=0, empty=1): 2,
             Markpat(last_turn=2, turn=1, empty=0): 1})
score 100 best score 100
APPEND
  best moves [(2, 0), (2, 1)]
====================
move (0, 2)
Turn x
x..
.o.
Oxo

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

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

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

から、ai10s は、最も評価値高い合法手 24いずれかの着手 を行うことが分かり、そのことは、下記の 実行結果最後の 2 行 からも 確認 できます。実際ai10s はそのうちの片方である 合法手 2選択 しました。

best score 100
best moves [(2, 0), (2, 1)]

合法手 2 または 4着手 を行うことで、「自 2 敵 1 空 0」が 2 つ存在 するようになるので、次の手番ai10s必ず勝利 できるようになります。従って、この後ai11s着手関わらずこの時点ai10s勝利が確定 し、実際の対戦 でも ai10s勝利 しています。そのため、この後着手の検証行う必要はない でしょう。

ai10s VS ai11s で行われる着手の選択のまとめ

上記の検証 から、ai10s VS ai11s で行われる 着手選択 は、下図 のようになることが分かります。なお、同一局面 がある場合は、そのうちの一つ局面のみ表示 します。

から わかる ように、ai10s VS ai11s では、3 手目まで は、必ず上図同一局面 になるような 着手行われます4 手目ai11s着手 で、50 %確率異なる着手選択 されます。4 手目上の着手行われた場合 は、5 手目ai10s2 通り着手 を行う 可能性 がありますが、何れの場合 でも ai10s100 % 勝利 します。

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

  • 3 手目 までは、常に同一局面 になる 着手 が行われる
  • 4 手目ai11s は、それぞれ 50 %確率2 通り着手 を行う
  • 4 手目ai11s が、上の着手 を行った場合は、ai10s必ず勝利 する。

このことから、ai10s VS ai11s の対戦で、ai11s敗北 するのは、4 手目上の着手行う可能性がある ためであることが 推測 されます。

なお、4 手目ai11s着手 で、下の着手 が行われた 場合どうなるか は、その 検証 をまだ 行っていない ので 現時点 では わかりません

長くなったので今回の記事はここまでにします。次回の記事では、下の着手 が行われた 場合 の検証と、ai11s上の着手行わないようにする ことで、ai10s負けないよう修正 する 方法 について 説明 します。

今回の記事のまとめ

今回の記事では、ai10s VS ai11sai11s負ける原因調べるため に、試合の 棋譜記録 するように Marubatsu クラスを 修正 しました。また、その後で、特定の AI どうし で、特定の結果 になる 試合経過表示する関数定義 しました。

また、その 関数を利用 して、ai10s VS ai11s で行われる 着手どのように選択されるか検証 し、ai11s敗北する原因 となる 可能性が高い着手発見 しました。

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

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

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

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

次回の記事

  1. 将棋やチェスでは、それぞれの プレイヤー は、特定の方向 から ゲーム盤見ます。そのため、ゲーム盤回転 したりすると、同一局面 では なくなります

  2. もちろん、検証しても、問題点や解決法が わからない場合 もあります

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