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を一から作成する その50 局面に対する評価値の設定と計算

Last updated at Posted at 2024-02-01

目次と前回の記事

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

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

これまでに作成した AI

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

関数名 アルゴリズム
ai1
ai1s
左上から順空いているマス を探し、最初に見つかったマス着手 する
ai2
ai2s
ランダム なマスに 着手 する
ai3
ai3s
真ん中 のマスに 優先的着手 する
既に 埋まっていた場合ランダム なマスに 着手 する
ai4
ai4s
真ん中 のマスの 優先的着手 する
既に 埋まっていた場合ランダム なマスに 着手 する
ai5 勝てる場合勝つ
そうでない場合は ランダム なマスに 着手 する
ai6 勝てる場合勝つ
そうでない場合は 相手の勝利阻止 する
そうでない場合は ランダム なマスに 着手 する
ai7 真ん中 のマスに 優先的着手 する
そうでない場合は 勝てる場合勝つ
そうでない場合は 相手の勝利阻止 する
そうでない場合は ランダム なマスに 着手 する

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

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分 欠陥
ai1 78.1 17.5 4.4 44.7 51.6 3.8 61.4 34.5 4.1 あり
ai2 58.7 28.8 12.6 29.1 58.6 12.3 43.9 43.7 12.5
ai3 69.3 19.2 11.5 38.9 47.6 13.5 54.1 33.4 12.5
ai4 83.0 9.5 7.4 57.2 33.0 9.7 70.1 21.3 8.6 あり
ai5 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
ai7 95.8 0.2 4.0 82.3 2.4 15.3 89.0 1.3 9.7

局面に対する評価

ルール 1 ~ 4 までは、真ん中のマスや、隅のマスなど、行った 着手の種類 に対して 評価値計算 しましたが、いずれも 着手した後局面状態 を全く 考慮に入れていません でした。少し考えればわかると思いますが、このような 局面考慮に入れない ルールで、本当に強い AI作る ことは 不可能 です。今回の記事では、ルール 5 ~ 7 の、合法手を 着手した後局面 に対する 評価値計算 の方法について説明します。

評価値を利用した ルール 5 で着手を行う AI の定義

下記の ルール 5 を、評価値を利用 した アルゴリズム実装 するために、どのような評価値設定 すれば良いかについて少し考えてみて下さい。

  • 勝てる場合勝つ
  • そうでない場合は ランダム なマスに 着手 する

評価値の設定

ルール 5 では、「自分が 勝利した 局面」の 評価値 を、「自分が 勝利していない 局面」の 評価値より高く設定 する必要があります。また、勝利 する 合法手存在しない 場合は ランダム なマスに 着手 するので、それら評価値すべて同じ値設定 する 必要 があります。なお、勘違いしやすい 点ですが、「自分が 勝利していない 局面」は、「自分 が 敗北 した局面」と 同じ意味ではない 点に注意して下さい。具体的には、「自分が 敗北 した 局面」だけでなく、「ゲームが 続行中」と「引き分け」の 局面含みます

そこで、本記事では、下記の表 のように 評価値設定 することにします。

局面の状況 評価値
自分勝利 している 1
自分勝利していない 0

評価値を利用したアルゴリズムの実装方法のおさらい

まず、評価値を利用 した アルゴリズム実装方法おさらい をします。

評価値を利用 した アルゴリズム では、以下 のような 処理 を行います。

  1. 現在 の、自分の手番局面 に対して、合法手着手 した 局面すべて計算 する
  2. それぞれの 合法手着手 した 局面 に対して 評価値を計算 する
  3. 最も評価値高い 局面になる 合法手 の中から ランダム選択 する

手順 13 は、ai_by_score で行うので、実際に 記述 する 必要 があるのは、手順 2 の処理を行う 評価関数実装 です。評価関数仮引数 には、合法手着手 した 局面 を表す Marubatsu クラスインスタンス代入 され、それを使って 評価値計算 します。

その 仮引数名前どのような名前 をつけても かまいません が、本記事では mb という 名前を付ける ことにし、以後は、評価関数説明 の中で、評価値計算 する 局面 を表す Marubatsu クラスインスタンスmb と記述することにします。

自分が勝利したかどうかの判定

評価関数 で、mb局面自分が勝利しているか どうかは、status 属性 が、自分のマーク表す値であるか どうかで 判定 することが できます が、mb相手の手番局面 なので、手番 を表す mb.turnmb.statusそのまま比較 しても、自分勝利しているか どうかを 判定 することは できません。そこで、以前の記事で定義した ai5 では、下記のプログラムの 7 行目 のように、mbstatus 属性 と、着手前自分の手番局面 を表す mb_orig の、turn 属性等しいか どうかで、その 判定行っています

1  def ai5(mb_orig):
2      legal_moves = mb_orig.calc_legal_moves()
3      for move in legal_moves:
4          mb = deepcopy(mb_orig)
5          x, y = move
6          mb.move(x, y)
7          if mb.status == mb_orig.turn:
8              return move
9      return choice(legal_moves)

しかし、評価値計算 する 評価関数仮引数 には、合法手着手後局面 を表す mb しか存在しない ので、上記のように 着手前局面手番比較 することは できません。本記事では 3 種類の方法を紹介しますが、評価関数の中 で、mb情報のみ を使って 自分勝利しているか どうかを 判定する方法 について少し考えてみて下さい。

本記事では採用しませんが、評価関数 に、合法手着手する前 の局面と、着手した後 の局面を 代入 する 2 の仮引数設定 するという方法もあります。その場合は、ai_by_score の中で 評価関数呼び出す 処理を 下記 のように 記述 します。

def ai_by_score(mb_orig, eval_func, debug=False, rand=True):

        score = eval_func(mb_orig, mb)

修正箇所
def ai_by_score(mb_orig, eval_func, debug=False, rand=True):

-       score = eval_func(mmb)
+       score = eval_func(mb_orig, mb)

そして、評価関数下記 のように 定義 します。

def eval_func(mb_orig, mb):
    評価関数の処理を記述する
修正箇所
-def eval_func(mb):
+def eval_func(mb_orig, mb):
    評価関数の処理を記述する

本記事でこの方法を 採用しない理由 は、関数の 仮引数の数多くなる と関数が 扱いづらく分かりづらく なる点と、評価関数 の中で、手番以外着手前 の局面の 情報 を使って 評価値計算しない からです。ただし、上記の方法を利用してもプログラムは 正しく動作 するので、こちらを採用してもかまいません。

判定方法その 1(自分の手番を計算する方法)

〇×ゲーム は、自分の手番相手の手番交互回ってくる ゲームなので、自分の手番 は、相手の手番 を表す mb.turn から 下記の式 で計算することができます。なお、この処理 は、move メソッド 内で、着手後手番を入れ替える 処理と 同じ です。

Marubastu.CROSS if mb.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE

従って、ルール 5着手 を行う ai5s は、下記のプログラムで実装できます。

  • 6 行目相手の手番 を表す mb.turn から、自分の手番 を計算し、my_turn代入 する
  • 7、8 行目:合法手を 着手後局面の状態 を表す mb.status が、自分の手番 を表す my_turn等しい 場合は 自分が勝利 しているので、評価値 として 1返す
  • 9、10 行目:そうでなければ、評価値 として 0返す
 1  from marubatsu import Marubatsu
 2  from ai import ai_by_score
 3
 4  def ai5s(mb, debug=False):
 5      def eval_func(mb):
 6          my_turn = Marubatsu.CROSS if mb.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
 7          if mb.status == my_turn:
 8              return 1
 9          else:
10              return 0
11
12      return ai_by_score(mb, eval_func, debug=debug)
行番号のないプログラム
from marubatsu import Marubatsu
from ai import ai_by_score

def ai5s(mb, debug=False):
    def eval_func(mb):
        my_turn = Marubatsu.CROSS if mb.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
        if mb.status == my_turn:
            return 1
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)

動作の確認

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

from ai import ai_match, ai5, ai6, ai7

ai_match(ai=[ai5s, ai5])

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

ai5s VS ai5
count     win    lose    draw
o        6815    2770     415
x        2699    6846     455
total    9514    9616     870

ratio     win    lose    draw
o       68.2%   27.7%    4.2%
x       27.0%   68.5%    4.5%
total   47.6%   48.1%    4.3%

判定方法その 2(自分の手番を利用しない方法)

〇×ゲーム には、以下 のような 性質 があります。

  • 〇 のプレイヤー勝利 するのは、〇 のプレイヤー着手 を行った 場合だけ である
  • × のプレイヤー勝利 するのは、× のプレイヤー着手 を行った 場合だけ である

上記を 言い換える と、以下 のようになります。

  • 〇 のプレイヤー着手 することで、× のプレイヤー勝利 することは 無い
  • × のプレイヤー着手 することで、〇 のプレイヤー勝利 することは 無い
  • 従って、自分着手 することで、相手勝利 することは 無い

上記 から、現在の局面手番 と、合法手を 着手後の局面状態mb.status 属性すべて組み合わせ にすると、下記のようになります。

現在の局面の手番 着手後の状態 mb.status の値
〇 の勝ち Marubatsu.CIRCLE
引き分け Marubatsu.DRAW
決着がついていない Marubatsu.PLAYING
× × の勝ち Marubatsu.CROSS
× 引き分け Marubatsu.DRAW
× 決着がついていない Marubatsu.PLAYING

上記の表で、現在の局面手番場合 に、mb.statusMarubatsu.CROSSなること無い 点に 注目 して下さい。この 性質 から、現在の局面手番 の場合に、着手後の局面〇 が勝利 することを、下記条件式判定 することができます。

mb.status == Marubatsu.CIRCLE or mb.status == Marubatsu.CROSS

条件式 に、余計mb.status == Marubatsu.CROSS記述 されているのが変だと思うかもしれませんが、現在の局面手番場合mb.statusMarubatsu.CROSSなること無い いので、mb.status == Marubatsu.CROSS常に False になります。従って、現在の局面手番場合 は、上記の式下記の式計算 を行います。

mb.status == Marubastu.CIRCLE or False

この 条件式計算結果 は、下記条件式同じ値 になるので、先程の条件式 で、現在の局面手番場合正しい判定行うことができる ことが 確認 できました。

mb.status == Marubastu.CIRCLE

具体的な説明は省略しますが、同様の理由 で、現在の局面手番× の場合も、下記同じ条件式× が勝利 することを 判定 することが できます

mb.status == Marubatsu.CIRCLE or mb.status == Marubatsu.CROSS

従って、ai5s は下記のプログラムのように記述できます。

  • 3 行目上記の条件式 で、自分勝利 していることを 判定 するように 修正 する
1  def ai5s(mb, debug=False):
2      def eval_func(mb):
3          if mb.status == Marubatsu.CIRCLE or mb.status == Marubatsu.CROSS:
4              return 1
5          else:
6              return 0
7
8      return ai_by_score(mb, eval_func, debug=debug)
行番号のないプログラム
def ai5s(mb, debug=False):
    def eval_func(mb):
        if mb.status == Marubatsu.CIRCLE or mb.status == Marubatsu.CROSS:
            return 1
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)
修正箇所
def ai5s(mb, debug=False):
    def eval_func(mb):
-       my_turn = Marubatsu.CROSS if mb.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
-       if mb.status == my_turn:
+       if mb.status == Marubatsu.CIRCLE or mb.status == Marubatsu.CROSS:
            return 1
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)

動作の確認

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

from ai import ai_match, ai5

ai_match(ai=[ai5s, ai5])

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

ai5s VS ai5
count     win    lose    draw
o        6841    2736     423
x        2677    6879     444
total    9518    9615     867

ratio     win    lose    draw
o       68.4%   27.4%    4.2%
x       26.8%   68.8%    4.4%
total   47.6%   48.1%    4.3%

判定方法その 3(直前の手番を属性に代入して取っておく)

先程紹介した、判定方法その 1その 2 には以下のような 欠点 があります。

判定方法その 1 の欠点

判定方法 その 1 の、評価関数の中自分の手番計算 する 方法 は、自分の手番情報 を使って 評価値を計算 する 評価関数 を定義する際に、下記 のプログラムを 毎回記述 する 必要 がある点が 面倒 です。また、実際ルール 67 の AI や、その後で 実装する予定AI でも、評価関数の中自分の手番情報必要 になります。

my_turn = Marubatsu.CROSS if mb.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE

判定方法その 2 の欠点

判定方法 その 2方法 には、確かに 自分の手番情報計算 する 必要がない という 利点 がありますが、自分が勝利 することを 判定 する 条件式求める までの 考え方論理ロジック)が 複雑 なため、以下のような欠点があります。

  • 慣れないと 条件式求める のが 大変
  • プログラムの意味分かりづらくなる
  • 勘違いなど で、間違った論理条件式記述 することによる バグ発生しやすい

直前の手番を属性に代入して取っておく方法

自分が勝利 したことを 判定 するために 必要 な、自分の手番情報 は、mb直前の局面手番情報 から 得る ことができますが、その 情報 は、直前の局面turn 属性代入されています。従って、その情報 を、move メソッド手番入れ替える処理行う前 に、何らかの属性代入 して 取っておく ことで、判定方法 その 1 のように、改めて その情報を 計算 する 必要なくなります。そこで、その 属性の名前 を、直前(last)の 手番(turn)の情報を 代入 することから、last_turn名付ける ことにします。

実は、この方法 は、move メソッド の中で、直前の着手情報last_move という 属性代入する のと 同じ考え方 なので、last_move同様の方法実装 できます。具体的には、last_move の処理は、Marubatsu クラスrestart メソッド と、move メソッド で行われているので、それらメソッドlast_turn処理を記述 します。

restart メソッドの修正

last_move 属性の 初期化処理 は、ゲームの初期化処理 を行う、下記の restart メソッドの 6 行目 で行われます。そこで、その次の 7 行目 に、last_turn 属性の 初期化処理記述 します。ゲーム開始時局面 より 前の局面存在しない ので、None初期化 します。

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

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

Marubatsu.restart = restart

move メソッドの修正

last_turn 属性の 更新 処理は、下記のプログラムのように、着手 を行う move メソッド3 行目 で、着手前手番 を表す self.turn代入 します。この処理は、4 行目の、手番次の手番入れ替える 処理を 行う前 で行う 必要 がある点に 注意 して下さい。

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

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

Marubatsu.move = move

ai5s の実装

上記の修正を行うことで、ai5s では、下記のプログラムの 3 行目 のように、last_turn 属性 を使って 自分が勝利 したことを 判定 できるようになります。

1  def ai5s(mb, debug=False):
2      def eval_func(mb):
3          if mb.status == mb.last_turn:
4              return 1
5          else:
6              return 0
7
8      return ai_by_score(mb, eval_func, debug=debug)
行番号のないプログラム
def ai5s(mb, debug=False):
    def eval_func(mb):
        if mb.status == mb.last_turn:
            return 1
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)
修正箇所
def ai5s(mb, debug=False):
    def eval_func(mb):
-       if mb.status == Marubatsu.CIRCLE or mb.status == Marubatsu.CROSS:
+       if mb.status == mb.last_turn:
            return 1
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)

動作の確認

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

ai_match(ai=[ai5s, ai5])

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

ai5s VS ai5
count     win    lose    draw
o        6885    2673     442
x        2698    6842     460
total    9583    9515     902

ratio     win    lose    draw
o       68.8%   26.7%    4.4%
x       27.0%   68.4%    4.6%
total   47.9%   47.6%    4.5%

ai5ai5s の計算時間の比較

前回の記事 で、「評価値を利用 した アルゴリズム」は、「それぞれの条件順番に判定 し、最初 にみつかった 条件を満たす着手選択 する」場合と 比較 して 処理時間がかかる場合 があるという説明を行いました。

そこで、ai5ai5s処理時間比較 することにします。

下記の、ai5 どうしの 対戦処理時間 は、筆者のパソコンでは 約 28.5 秒 でした。

ai_match(ai=[ai5, ai5])

下記の、ai5s どうしの 対戦処理時間 は、筆者のパソコンでは 約 30.7 秒 でした。

ai_match(ai=[ai5s, ai5s])

前回の記事では、ai1 ~ ai4 と、対応 する ai1s ~ ai4s処理時間約 10 倍以上 がありましたが、ai5ai5s処理時間数 % に過ぎません。その 理由 は、ai1 ~ ai4 が、Marubatsu クラスインスタンス深いコピー作成 するという、時間がかかる処理行っていない のに対して、ai5その処理行っている からです。

ai5 のほうが、ai5s より も若干 処理時間短い のは、ai5 は、自分が勝利す する 合法手見つかった時点 は、その合法手採用 して 残りの処理行わない のに対して、ai5s は必ず すべて合法手着手行う からです。

ai5ai5s が選択する着手の違い

ai5ai5s は、対戦結果 からわかるように、どちらも 全く同じ強さ を持ちますが、状況によって、異なる方法着手選択 する 場合あります。具体的には、自分が勝利 できる 着手複数存在 する 場合 に、それぞれの AI は、下記 の方法で 着手選択 します。なお、それ以外場合 は、どちらの AI も 同じ方法着手選択 します。

  • ai5 は、最初見つかった 、自分が勝利できる合法手を 選択 する
  • ai5s は、自分が勝利できる合法手の中から、ランダム選択 する

ai5処理速度ai5s より 速い理由 は、上記の 処理の違い によるものです。

下記は、上記の性質確認 するプログラムで、〇 が勝利 できる 合法手3 つ存在 する 局面 で、ai5ai5s10 回 ずつ 着手を選択 します。実行結果 からわかるように、ai5常に同じ着手選択 しますが、ai5s3 つ合法手 から ランダムに選択 します。

mb = Marubatsu()

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

print("ai5")
for i in range(10):
    print(ai5(mb))
    
print("ai5s")
for i in range(10):
    print(ai5s(mb))

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

Turn o
oxx
oo.
.X.

ai5
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
ai5s
(2, 2)
(2, 1)
(2, 1)
(0, 2)
(2, 2)
(2, 1)
(2, 2)
(2, 2)
(2, 1)
(2, 2)

処理時間短くなる ことは ありません が、ai5s最後の行ai_by_score実引数 に、下記のように rand=False記述 することで、ai5同様 に、最初にみつかった 自分が勝利できる 合法手選択 する AI を定義 することが できます

def ai5s(mb, debug=False):

    return ai_by_score(mb, eval_func, debug=debug, rand=False)
修正箇所
def ai5s(mb, debug=False):

-   return ai_by_score(mb, eval_func, debug=debug)
+   return ai_by_score(mb, eval_func, debug=debug, rand=False)

ai5ai5s のどちらを選択すべきか

ai5ai5sどちらを選択すべきか については、それぞれ の AI の 利点欠点 を見て 総合的に判断 する必要があります。例えば、処理速度速さ非常に重要 な場合は、ai5選択すべき ですが、実際 には先程示したように、ai5ai5s処理速度数 % に過ぎないので、どちらを選択してもそれほど 処理速度大きな差生じません

実装のしやすさ の観点でみると、ai5s のほうが 実装しやすい でしょう。

ai5ai5s は、どちらも AI の強さ という 観点 でみると、同じ強さ を持ちますが、人間と AI対戦 する場合は、結果が同じ である としてもランダム性 のある 着手 を行う AI のほうが 好まれる のではないかと思います。同じ状況 で、必ず 同じ着手 しか行わない AI は、対戦していて つまらない と思いませんか?

上記以外にも、様々な観点比較 することができます。状況 によって どのような性質望ましいか変わる ので、結論人それぞれ でしょう。どちらを選択 するかについては、自分で考えて下さい

評価値を利用した ルール 6 で着手を行う AI の定義

ルール 6 は、ルール 5 に、相手の勝利を阻止 するという 条件加えた ものです。下記の ルール 6評価値どのように設定 すればよいかについて少し考えてみて下さい。

  • 勝てる場合勝つ
  • そうでない場合は 相手の勝利阻止 する
  • そうでない場合は ランダム なマスに 着手 する

ルール 6 では、「自分が勝利 した局面」、「相手の勝利を阻止 した局面」、「それ以外 の局面」の 評価値高く設定 する必要があります。そこで、下記の表 のように 評価値設定 することにします。

局面の状況 評価値
自分が勝利している 2
相手の勝利を阻止した 1
それ以外の場合 0

上記のうち、自分が勝利 している 局面 に対して、2評価値設定 する 処理 は、ai5s同じ方法記述 できるので、ai6s は、下記のようなプログラムで記述できます。

def ai6s(mb, debug=False):
    def eval_func(mb):
        if mb.status == mb.last_turn:
            return 2
        elif 相手の勝利を阻止したことを表す条件式:
            return 1
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)
修正箇所
-def ai5s(mb, debug=False):
+def ai6s(mb, debug=False):
    def eval_func(mb):
        if mb.status == mb.last_turn:
-           return 1
+           return 2
+       elif 相手の勝利を阻止したことを表す条件式:
+           return 1
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)

相手の勝利を阻止する着手を選択する別のアルゴリズム

ai6 では、最終的に下記のような、正攻法ではない、すこし ひねった考え方相手の勝利を阻止 する 着手選択 しました。忘れた方は、以前の記事の説明を見て復習して下さい。

現在の局面相手の手番みなし合法手 の中で、相手が着手 して 勝利 する マス を、相手の勝利を阻止 する 着手 として 選択 する」

残念ながら、評価値計算 する 評価関数仮引数 には、現在の局面 に対して 合法手着手 した 局面 のデータ しか代入されない ので、現在の局面相手の手番みなす という 処理 を行うことは できません。また、上記の考え方改良 する ルール 6 のアルゴリズム は、以前の記事で説明したように、着手の処理 を行う 多くなる ため、処理大きな時間がかかる という 欠点 があります。

そこで、今回の記事では、評価値を利用 する アルゴリズム で、ルール 6 による 着手効率よく選択 する 方法 を紹介します。

ルール 6 のアルゴリズムの問題点

下記は、現在の局面を相手の手番とみなすという、改良を行う前ルール 6 のアルゴリズム の再掲です。この中の、手順 3相手の勝利を阻止 する 着手選択 する処理です。

  1. 現在の局面合法手の一覧 から 順番合法手取り出し着手 を行う
  2. 自分が勝利 する 合法手 があれば、それを採用 して 終了 する
  3. 上記の 手順 1着手 を行った それぞれの局面 に対して、下記の処理 を行う
    1. 合法手の一覧 から 順番合法手取り出し着手 を行う
    2. 相手が勝利 する 合法手 があれば、それを採用 して 終了 する
  4. 上記の 手順 1 ~ 3処理すべて終了 した時点で、自分が勝利 できる合法手も、相手が勝利 できる合法手も 存在しない ことが 確定 するので、ランダムな着手 を行う

このアルゴリズムの 処理大きな時間 がかかる 理由 は、現在の局面 から、2 手分すべて合法手組み合わせ で実際に 着手 を行う 必要 がある点にあります1。そこで、上記のアルゴリズムを 改良 して、ai_by_score の処理が行う、現在の局面 から 1 手分合法手着手 した 局面の評価値だけ着手を選択 することができるようにします。

ルール 6 改の定義

ルール 62 つ目条件目的 は、「相手の勝利阻止 する着手を行う」というものでしたが、これは、「相手勝利できる 着手を 行わない」と 言い換える ことができます。また、具体的な方法はこの後で説明しますが、「相手勝利できる 着手を 行わない」という条件は、1 手分合法手着手 した 局面情報だけ から 判定 することが できます

ルール 6条件 2変更 した下記のルールを、ルール 6 改 とする。

  • 勝てる場合勝つ
  • そうでない場合は 相手勝利できる 着手を 行わない
  • そうでない場合は ランダム なマスに 着手 する

ただし、このような 条件の言い換え は、間違っている可能性がある ので、本当にそのように言い換えることができるかどうかについて 検証 して 確認 することにします。

表記が長いので、以後は「相手の勝利阻止 する着手を行う」条件を「元の条件」、「相手勝利できる 着手を 行わない」条件を「改良した条件」と 表記 することにします。

ルール 6 も、ルール 6 改 も、条件 1 の「勝てる場合に勝つ 」は 同じ なので、以後の説明は、自分が勝利していない局面 に対する 説明 です。

元の条件 は、「相手の勝利阻止 する着手を行う」という 条件 ですが、この 条件を元 に、〇×ゲームの 局面分類 すると、下記の 3 つに分類 することができます。そこで、この 3 つの それぞれの場合 で、両方の条件どのような着手行われるか検証 します。

  • 相手の勝利を阻止 する 合法手存在しない 場合
  • 相手の勝利を阻止 する 合法手1 つ だけある場合
  • 相手の勝利を阻止 する 合法手2 つ以上 ある場合

相手の勝利を阻止する合法手が存在しない場合

元の条件 の場合は、条件を満たす合法手存在しない ので、ルール 6 の 「そうでない場合は ランダム なマスに 着手 する」という 条件 3 が適用され、合法手 の中から ランダムな着手選択 することになります。

相手の勝利を阻止 する 合法手存在しない ということは、次の 相手の手番相手が勝利 する ことがない ということを表します。従って、すべての合法手改良した条件 である「相手勝利できる 着手を 行わない」を満たすので、改良した条件 の場合も 合法手 の中から ランダムな着手選択 することになります。

上記 から、どちらの条件も同じ方法着手を選択 することが 確認 できました。

相手の勝利を阻止する合法手が 1 つだけある場合

この場合の 局面 は、以下 のような 状況 の局面です。

  • その合法手選択 すると、次の相手の手番相手が勝利 することは 無い
  • その合法手 以外選択 すると、次の相手の手番相手勝利できる

元の条件 の場合は、その合法手選択する ことになります。

改良した条件 の場合は、その合法手以外 の合法手を 選択しない ので、やはり その合法手選択 することになります。

上記 から、どちらの条件も同じ方法着手を選択 することが 確認 できました。

相手の勝利を阻止する合法手が 2 つ以上ある場合

この場合の 局面 は、以下 のような 状況 の局面です。下記からわかるように、どの合法手着手 しても、次の相手の手番相手が勝利 できます。

  • 相手の勝利を阻止 する 合法手 中の どれを選択 しても、次の相手の手番選択されなかった 別の 合法手着手 して 相手勝利できる
  • その合法手 以外選択 すると、次の相手の手番相手勝利できる

元の条件 の場合は、相手の勝利阻止 する 合法手いずれか選択 します。ai6 の場合は、最初に見つかった合法手選択 しますが、ランダムに選択しても構いません。

改良した条件 の場合は、条件を満たす合法手存在しない ので、ルール 6条件 3 が適用され、すべて合法手 の中から ランダムな着手選択 することになります。

上記から、元の条件改良した条件 で、選択される着手異なる ことが 分かります。そのため、この 違い によって、AI の強さが変わる かどうかを 検証 する必要があります。

AI の強さが変わるかどうかの検証

相手の勝利を阻止 する 合法手2 つ以上 ある場合に、元の条件 と、改良した条件 によって選択される着手の 違い は以下のようになります。

  • 元の条件 の場合は、相手が勝利 する 合法手着手 を行うので、相手の手番相手勝利できる 合法手の が必ず 1 つ減る
  • 改良した条件 の場合は、ランダムな着手 を行うので、相手の手番相手勝利できる 合法手の 1 つ減る 場合と、減らない 場合がある

相手の AI が、勝てる場合に勝つ という ルール着手選択 する場合は、どちらの条件 でも 相手が勝利 することに変わりはありません。

一方、相手の AI が、勝てる場合に勝つ という ルール着手選択しない 場合は、相手勝利しない 着手を行う 可能性 が生じます。例えば、相手の AIランダムな着手 を行う場合は、相手勝利できる 合法手の 数が多いほうが相手が勝利 する 確率高くなります。従って、必ず 相手が 勝利できる 合法手の 数が減る という、元の条件 のほうが、相手が勝利 する 確率低くなる ことになります。そのことを 確認 するために、ai6s実装後 に、常にランダムな着手 を行う ai2ai6s対戦 することにします。

下記は、上記の考察をまとめたものです。

  • 相手が「勝てる場合に勝つ」というルールを 持つ 場合は、ルール 6ルール 6 改 の AI は 同じ強さ を持つ
  • 相手が「勝てる場合に勝つ」というルールを 持たない 場合は、ルール 6 のほうが ルール 6 改 よりほんの少しだけ強い

ルール 6 改 のほうが 弱い場合がある のは 問題 だと 思う 人が いるかもしれません が、実際には このことはほとんど 問題にはなりません。その理由は、勝てる場合に勝つ という ルール が、〇× ゲームの 初心者 でも すぐに思いつく ような あまりにも簡単なルール であり、強い AI を作る際に、必須 となる ルール だからです。従って、ある程度強い AI であれば、「勝てる場合に勝つ」という ルール必ず持っている ので、そのようなルールを持たない 弱い AI多少弱く なっても 全く問題ない と考えることができます。

元の条件と改良した条件で AI の強さが変わらない理由

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

相手の勝利を阻止する合法手 着手の方法 強さへの影響
存在しない 同じ方法で着手を行う なし
1 つだけ存在する 同じ方法で着手を行う なし
2 つ以上存在する 異なる方法で着手を行う ある程度相手が強ければ、
なしとみなすことができる

表から、すべての場合 で、どちらの方法着手を選択 しても、基本的 には AI の強さ影響を及ぼさない ことが分かります。従って、元の条件 を、改良した条件言い換えてもルール 6同じ強さAI実装できる ことが 確認 できました。

評価値の再設定

ルール 6 の条件変更 したので、評価値設定方法 もそれに合わせて 修正 する 必要 があります。ルール 6 改以前条件 は、何らかの 条件を満たす 着手を 選択する というものでした。その場合は、条件を満たす 着手を行った 局面評価値高く 設定します。

一方、ルール 6 改条件 2 は、何らかの 条件を満た す着手を 選択しない というものです。この場合は、条件を満たす 着手を行った 局面評価値低く 設定します。

従って、評価値 を下記の表のように 設定 することができます。なお、相手が勝利できる ということは、自分にとって 不利な状況 を表すので、評価値 には 負の値 である -1 を設定しました。また、それに合わせて、自分が勝利 している局面の 評価値1 に修正 しました。

局面の状況 評価値
自分が勝利している 1
相手が勝利できる -1
それ以外の場合 0

なお、表の 評価値の列 の数字は、大きい順 に並んでいた方が 分かりやすく間違えにくくなる ので、そのように 行の順番入れ替える ことにします。ただし、下記の表のように、単純に入れ替えてしまうと、「それ以外の場合」の行の 意味わからなくなる ので、その行の 説明言い換える 必要があります。

局面の状況 評価値
自分が勝利している 1
それ以外の場合 0
相手が勝利できる -1

それ以外の場合」とは、「相手が勝利できない」という状況なので、下記の表のように修正すれば良いと思うかもしれませんが、下記の表 には 問題 があります。

局面の状況 評価値
自分が勝利している 1
相手が勝利できない 0
相手が勝利できる -1

これまでの表 では、上にある行 のほうが 優先順位が高い 条件でしたが、上記の表は、「2 行目 > 4 行目 > 3 行目」の順で優先順位が高くなっており、ルールの 条件優先順位高い順並んでいません。上記のような、条件の数が少ない場合は誤解は発生しないかもしれませんが、今後、条件の数増えた場合 に、この 表を見て プログラムを 記述する と、優先順位間違い による バグが発生 する 可能性が高く なります。そこで、下記の表のように、優先順位 を表す 列を追加 することにします。

優先順位 局面の状況 評価値
1 自分が勝利している 1
3 相手が勝利できない 0
2 相手が勝利できる -1

次の相手の着手で相手が勝利するかどうかを判定する方法

上記のような 評価値計算 するためには、相手の着手相手勝利できるか どうかを 判定 する必要があります。その方法について少し考えてみて下さい。

〇×ゲーム で、相手の着手相手が勝利 するという 局面 は、縦 3 種類横 3 種類斜め 2 種類計 8 種類 ある 一直線上3 マス のうち、「相手のマークが配置 されている マスが 2 つ と、空白のマス1 つ ある」ものが 存在する場合 です。従って、そのことを判定 することで、相手の着手相手が勝利できるか どうかを 判定 することが できます

一直線上3 マス配置 されている、自分のマーク数を数える 処理は、judge メソッド実装する際定義 した is_same メソッド実装方法の一つ2として、以前の記事で紹介した 下記のプログラム の中で 記述 しました。is_same は、以前の記事 で説明した、差分で座標を計算 する アルゴリズム を使っているので、忘れた方は復習して下さい。

具体的には、is_same は、coord という 座標マスから始まりdxdy差分 とする 一直線上3 つのマスmark のマークが 配置 されている 数を数えcount という変数に 代入 する 処理 を行います。

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

    return count == self.BOARD_SIZE

count_marks の定義

is_same参考 にして、下記の メソッドMarubatsu クラス定義 する事にします。

名前マークの数数える ので、count_marks という名前にする
処理〇 のマーク× のマーク空のマスそれぞれの数数える
入力:入力として、is_same同じ仮引数 を持つようにする。ただし、count_marksすべてのマーク数える ので、仮引数 mark必要がない ので 削除 する
出力それぞれ数えた数要素 として持つ defaultdict を返す

それぞれ数を数える処理 は、以前の記事で紹介した、defaultdict利用した集計同じ方法 で行うことができるので、count_marks は下記のプログラムのように定義できます。

  • 5 行目×空のマス数える ための ローカル変数 count を、既定値0defaultdict初期化 する
  • 7 行目count の、(x, y) のマスの マークキー とする キーの値1 増やす
  • 11 行目count返り値 として 返す
 1  from collections import defaultdict
 2
 3  def count_marks(self, coord, dx, dy):
 4      x, y = coord   
 5      count = defaultdict(int)
 6      for _ in range(self.BOARD_SIZE):
 7          count[self.board[x][y]] += 1
 8          x += dx
 9          y += dy
10
11       return count
12
13  Marubatsu.count_marks = count_marks
行番号のないプログラム
from collections import defaultdict

def count_marks(self, coord, dx, dy):
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

    return count

Marubatsu.count_marks = count_marks
修正箇所(is_same との違いです)
from collections import defaultdict

-def is_same(self, mark, coord, dx, dy):
+def count_marks(self, coord, dx, dy):
    x, y = coord   
-   count = 0
+   count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
-       if self.board[x][y] == mark:
-           count += 1
+       count[self.board[x][y]] += 1
        x += dx
        y += dy

    return count

Marubatsu.count_marks = count_marks

下記は、いくつかの局面 に対して、一番上の行3 マス配置 された ×空のマス を、count_marks数えた結果表示 するプログラムです。count_marks に記述した 実引数意味が分からない 方は、以前の記事を復習して下さい。実行結果 から、正しく数えている ことが 確認 できます。

mb = Marubatsu()

print(mb)
print(mb.count_marks((0, 0), 1, 0))

mb.move(0, 0)
print(mb)
print(mb.count_marks((0, 0), 1, 0))

mb.move(1, 0)
print(mb)
print(mb.count_marks((0, 0), 1, 0))

mb.move(2, 0)
print(mb)
print(mb.count_marks((0, 0), 1, 0))

実行結果

Turn o
...
...
...

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

defaultdict(<class 'int'>, {'o': 1, '.': 2})
Turn o
oX.
...
...

defaultdict(<class 'int'>, {'o': 1, 'x': 1, '.': 1})
Turn x
oxO
...
...

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

なお、defaultdictprint で表示すると、下記 のように 表示 されます。上記の 実行結果見比べて下さい

defaultdict(既定値に関する情報, dict の情報)

ai6s の定義

8 種類一直線上3 マスマークの数数える方法 は、下記の Marubatsu クラスの is_winner メソッドと 同様の方法 で行うことができます。

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

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

従って、ai6s は、is_winner参考 に、下記のプログラムのように記述できます。ただし、下記の 11141822 行目 の「次の相手の局面で相手が勝利できる」の 条件式未完成 です。その 条件式どのように記述 すればよいかについて少し考えてみて下さい。

 1  def ai6s(mb, debug=False):
 2      def eval_func(mb):
 3          # 自分が勝利している場合は、評価値として 1 を返す
 4          if mb.status == mb.last_turn:
 5              return 1
 6
 7          # 相手の手番で相手が勝利できる場合は評価値として -1 を返す
 8          # 横方向と縦方向の判定
 9          for i in range(mb.BOARD_SIZE):
10              count = mb.count_marks(coord=[0, i], dx=1, dy=0)
11              if 次の相手の局面で相手が勝利できる:
12                  return -1
13              count = mb.count_marks(coord=[i, 0], dx=0, dy=1)
14              if 次の相手の局面で相手が勝利できる:
15                  return -1
16          # 左上から右下方向の判定
17          count = mb.count_marks(coord=[0, 0], dx=1, dy=1)
18          if 次の相手の局面で相手が勝利できる:
19              return -1
20          # 右上から左下方向の判定
21          count = mb.count_marks(coord=[2, 0], dx=-1, dy=1)
22          if 次の相手の局面で相手が勝利できる:
23              return -1
24
25          # それ以外の場合は評価値として 0 を返す
26          return 0
27
28      return ai_by_score(mb, eval_func, debug=debug)

次の相手の手番相手が勝利 できる 条件 は、一直線上3 マス相手のマーク2 つ空のマス1 つ の場合です。また、mb相手の手番局面 なので、相手のマーク は、mb.turn代入 されています。従って、下記の 条件式判定 できます。

if count[mb.turn] == 2 and count[Marubatsu.EMPTY] == 1:

この 条件式当てはめる ことで、ai6s を下記のプログラムのように 定義 できます。

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

        # 相手の手番で相手が勝利できる場合は評価値として -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

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

    return ai_by_score(mb, eval_func, debug=debug)        
修正箇所
def ai6s(mb, debug=False):
    def eval_func(mb):
        # 自分が勝利している場合は、評価値として 1 を返す
        if mb.status == mb.last_turn:
            return 1

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

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

    return ai_by_score(mb, eval_func, debug=debug)      

動作の確認

ai6s正しく動作 するかどうかを 確認 するために、ai6対戦 を行います。ai6 は、「勝てる場合に勝つ」という ルール持つ ので、ai6ai6s同じ強さ を持つはずです。実行結果 から、ai6s正しく実装 できていることが 確認 できました。

ai_match(ai=[ai6s, ai6])

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

ai6s VS ai6
count     win    lose    draw
o        3161    1672    5167
x        1723    3114    5163
total    4884    4786   10330

ratio     win    lose    draw
o       31.6%   16.7%   51.7%
x       17.2%   31.1%   51.6%
total   24.4%   23.9%   51.6%

ai6ai6s が選択する着手の違い

先程説明したように、ai6ai6s は、「相手の勝利を阻止 する 合法手2 つ以上 ある」場合は、異なる方法着手 を行います。そのことを下記のプログラムで 確認 します。

実行結果 からわかるように、ai6 は、2 つ ある 相手の勝利を阻止 する 合法手 のうちの (2, 1)必ず着手 を行いますが、ai6s は、どこに着手 を行っても 相手の勝利阻止できない ので、ランダムな着手 を行った結果、(1, 2) という、相手の勝利阻止しない着手行う ので、いずれの場合 でも 相手が勝利 することに 変わりはありません

mb = Marubatsu()

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

print("ai6")
for i in range(10):
    print(ai6(mb))
    
print("ai6s")
for i in range(10):
    print(ai6s(mb))

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

Turn x
oxx
Oo.
...

ai6
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
(2, 1)
ai6s
(1, 2)
(2, 1)
(2, 1)
(2, 1)
(2, 2)
(1, 2)
(2, 1)
(0, 2)
(1, 2)
(2, 2)

なお、強さ という 観点 では、ai6ai6s同じ強さを 持ちますが、人間対戦 した場合は、ai6s が、敗北確実 になった場合に 最善を尽くさない投げやりな着手 を行うように 見える かもしれません。人間の心理 として、負けるとわかってる場合 でも、最善を尽くさなければ相手に失礼 だというものがあるからです。ai6sai6同様の着手 を行うようにする方法については今後の記事で紹介します。

ai2 との対戦

さきほど、常にランダム着手 を行う ai2 に対しては、ai6sai6 より若干弱くなる という説明を行いましたので、そのことを下記のプログラムで 確認 することにします。

ai_match(ai=[ai7s, ai7])

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

ai6s VS ai2
count     win    lose    draw
o        8858     195     947
x        6937     912    2151
total   15795    1107    3098

ratio     win    lose    draw
o       88.6%    1.9%    9.5%
x       69.4%    9.1%   21.5%
total   79.0%    5.5%   15.5%

下記は、ai6 VS ai2ai6s VS ai2対戦結果 を表にしたものです。実行結果 から、〇 を担当 する場合は 成績ほとんど変わりません が、× を担当 する場合は、ai6sのほうai6 より 少しだけ 敗率が 悪い ことが 確認 できます。このような結果になる 理由 は、おそらく 〇 を担当 する場合は、ルール 6ルール 6 改違いが影響する ような 局面ほとんど生じない からだと 推測 されます。

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分 欠陥
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

評価値を利用した ルール 7 で着手を行う AI の定義

ルール 7 は、下記のように、ルール 6 に、真ん中 のマスに 優先的着手 するという 条件加えた ものです。

  • 真ん中 のマスに 優先的着手 する
  • 勝てる場合勝つ
  • そうでない場合は 相手の勝利阻止 する
  • そうでない場合は ランダム なマスに 着手 する

ai6s同様の方法定義 できるように、ルール 7条件 3 を下記のように 言い換えた、下記の ルール 7 改ai7s定義 する事にします。難しくはない と思いますが、評価値どのように設定 すればよいかについて少し考えてみて下さい。

  • 真ん中 のマスに 優先的着手 する
  • 勝てる場合勝つ
  • そうでない場合は 相手勝利できる 着手を 行わない
  • そうでない場合は ランダム なマスに 着手 する

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

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

なお、以前の記事 で示したように、ルール 7着手行う 場合に、真ん中 のマスに 着手 した 時点自分が勝利 することは あり得ない ので、下記の表のように、「真ん中のマスに着手 する」局面と、「自分が勝利している」局面の 評価値同じ設定 しても 構いません。なお、本記事では上記の表で評価値を設定することにします。

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

ai7s の定義

下記は、ai7s定義 するプログラムです。特に 難しい点はない と思います。

  • 4、5 行目真ん中 のマスに 着手 した場合は、評価値 として 2返す 処理を 追加 する
1  def ai7s(mb, debug=False):
2      def eval_func(mb):
3          # 真ん中のマスに着手している場合は、評価値として 2 を返す
4          if mb.last_move == (1, 1):
5              return 2
6    
以下は ai6s と同じなので省略       
行番号のないプログラム
def ai7s(mb, debug=False):
    def eval_func(mb):
        # 真ん中のマスに着手している場合は、評価値として 2 を返す
        if mb.last_move == (1, 1):
            return 2
    
        # 自分が勝利している場合は、評価値として 1 を返す
        if mb.status == mb.last_turn:
            return 1

        # 相手の手番で相手が勝利できる場合は評価値として -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

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

    return ai_by_score(mb, eval_func, debug=debug)      
修正箇所
-def ai6s(mb, debug=False):
+def ai7s(mb, debug=False):
    def eval_func(mb):
+       # 真ん中のマスに着手している場合は、評価値として 2 を返す
+       if mb.last_move == (1, 1):
+           return 2

以下は ai6s と同じなので省略

なお、ai7 では、その ブロックの中ai6 を呼び出す ことで、簡潔 なプログラムで 定義 することが できました が、ai6s評価関数 は、ローカル関数 として 定義 しているので、ai7sブロックの中 で、ai6s評価関数利用 することは できません。そのため、ai7sai7 のように 簡潔に記述 することは できません

ai7sai7 のように 簡潔に記述 する場合は、ai6s評価関数グローバル関数 として 定義 する必要があります。ただし、ai6s評価関数 は、ai6sai7s 以外利用する予定はない ので、本記事ではグローバル関数で定義しません。

動作の確認

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

ai_match(ai=[ai7s, ai7])

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

ai7s VS ai7
count     win    lose    draw
o        2950     416    6634
x         417    2910    6673
total    3367    3326   13307

ratio     win    lose    draw
o       29.5%    4.2%   66.3%
x        4.2%   29.1%   66.7%
total   16.8%   16.6%   66.5%

本記事では確認しませんが、ai7s は、ai6s同様の理由 で、ai2 に対する成績 は、ai7 より若干悪く なります。興味がある方は実際に確認してみて下さい。

今回の記事のまとめ

今回の記事では、局面に対する評価値の 設定方法 と、計算方法 について 説明 し、ルール 5 ~ 7AI評価値を利用 した アルゴリズム で定義しました。その際に、ルール 67条件 の一つを 言い換えたルール 6 改7 改定義 しました。

また、同じルール着手選択 する AI でも、アルゴリズム違い によって、AI の強さ変わらない が、着手選択方法異なる場合がある ことを 説明 しました。

次回の記事では、評価値を利用 した アルゴリズム で、より強い AI を作成する方法について説明します。

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

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

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

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

次回の記事

  1. 実際には、途中着手を選択 する 場合があり、その場合はすべての着手を行う必要はありませんが、多くの場合すべての着手行う必要 があります

  2. 実際の Marubatsu クラスの is_same メソッドは、これとは別の方法で実装しています。

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?