Pythonで〇×ゲームのAIを一から作成する　その50　局面に対する評価値の設定と計算

# 目次と前回の記事

https://qiita.com/ysgeso/items/2381dd4e3283cbed49a0

https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf

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

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

https://github.com/ysgeso/marubatsu/blob/master/049/marubatsu.py

https://github.com/ysgeso/marubatsu/blob/master/049/ai.py

## これまでに作成した AI

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

| 関数名 | アルゴリズム |
|:-:|:--|
| __[`ai1`](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#ai1-%E3%81%AE%E5%AE%9A%E7%BE%A9)__<br>__[`ai1s`](https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf#ai1s-%E3%81%AE%E4%BF%AE%E6%AD%A3)__  | __左上から順__ に __空いているマス__ を探し、__最初に見つかったマス__ に __着手__ する |
| __[`ai2`](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#ai2-%E3%81%AE%E5%AE%9A%E7%BE%A9)__<br>__[`ai2s`](https://qiita.com/ysgeso/items/8f3d4b3d2c02ef51a70e#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%A2%E6%95%B0%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E3%83%AB%E3%83%BC%E3%83%AB-2-%E3%81%A7%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%86-ai-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __ランダム__ なマスに __着手__ する |
| __[`ai3`](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#ai3-%E3%81%AE%E5%AE%9F%E8%A3%85)__<br>__[`ai3s`](https://qiita.com/ysgeso/items/8f3d4b3d2c02ef51a70e#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%A2%E6%95%B0)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>既に __埋まっていた場合__ は __ランダム__ なマスに __着手__ する | |
| __[`ai4`](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#%E3%83%AB%E3%83%BC%E3%83%AB-4%E5%9B%9B%E9%9A%85%E3%81%AE%E3%83%9E%E3%82%B9%E3%81%AB%E5%84%AA%E5%85%88%E3%81%97%E3%81%A6%E7%9D%80%E6%89%8B%E3%81%99%E3%82%8B)__<br>__[`ai4s`](https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf#ai4s-%E3%81%AE%E4%BF%AE%E6%AD%A3%E3%81%A8%E5%8B%95%E4%BD%9C%E3%81%AE%E7%A2%BA%E8%AA%8D)__  | __真ん中__、__隅__ のマスの __順__ で __優先的__ に __着手__ する<br>既に __埋まっていた場合__ は __ランダム__ なマスに __着手__ する | あり |
| __[`ai5`](https://qiita.com/ysgeso/items/ff28a95dff50322e6b16#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-5%E5%8B%9D%E3%81%A6%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AB%E5%8B%9D%E3%81%A4)__  | __勝てる場合__ に __勝つ__<br>そうでない場合は __ランダム__ なマスに __着手__ する | |
| __[`ai6`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-6%E7%9B%B8%E6%89%8B%E3%81%AE%E5%8B%9D%E5%88%A9%E3%82%92%E9%98%BB%E6%AD%A2%E3%81%99%E3%82%8B)__  | __勝てる場合__ に __勝つ__<br>そうでない場合は __相手の勝利__ を __阻止__ する<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[`ai7`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-7%E3%83%AB%E3%83%BC%E3%83%AB3-%E3%81%A8-%E3%83%AB%E3%83%BC%E3%83%AB-6-%E3%81%AE%E4%BD%B5%E7%94%A8)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手の勝利__ を __阻止__ する<br>そうでない場合は __ランダム__ なマスに __着手__ する |

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

| 関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 | 欠陥 |
|:-:|--:|--:|--:|--:|--:|--:|--:|--:|--:|:-:|
| __[`ai1`](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#ai1-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __78.1__ | __17.5__ | 4.4 | __44.7__ | __51.6__ | 3.8 | __61.4__ | __34.5__ | 4.1 | あり |
| __[`ai2`](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#ai2-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | 58.7 | 28.8 | 12.6 | 29.1 | 58.6 | 12.3 | 43.9 | 43.7 | 12.5 | |
| __[`ai3`](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#ai3-%E3%81%AE%E5%AE%9F%E8%A3%85)__  | __69.3__ | __19.2__ | 11.5 | __38.9__ | __47.6__ | 13.5 | __54.1__ | __33.4__ | 12.5 | |
| __[`ai4`](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#%E3%83%AB%E3%83%BC%E3%83%AB-4%E5%9B%9B%E9%9A%85%E3%81%AE%E3%83%9E%E3%82%B9%E3%81%AB%E5%84%AA%E5%85%88%E3%81%97%E3%81%A6%E7%9D%80%E6%89%8B%E3%81%99%E3%82%8B)__  | __83.0__ | __9.5__ | 7.4 | __57.2__ | __33.0__ | 9.7 | __70.1__ | __21.3__ | 8.6 | あり |
| __[`ai5`](https://qiita.com/ysgeso/items/ff28a95dff50322e6b16#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-5%E5%8B%9D%E3%81%A6%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AB%E5%8B%9D%E3%81%A4)__  | __81.2__ | __12.3__ | 6.5 | __51.8__ | __39.8__ | 8.4 | __66.5__ | __26.0__ | 7.4 | |
| __[`ai6`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-6%E7%9B%B8%E6%89%8B%E3%81%AE%E5%8B%9D%E5%88%A9%E3%82%92%E9%98%BB%E6%AD%A2%E3%81%99%E3%82%8B)__  | __88.9__ | __2.2__ | 8.9 | __70.3__ | __6.2__ | 23.5 | __79.6__ | __4.2__ | 16.2 |
| __[`ai7`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-7%E3%83%AB%E3%83%BC%E3%83%AB3-%E3%81%A8-%E3%83%AB%E3%83%BC%E3%83%AB-6-%E3%81%AE%E4%BD%B5%E7%94%A8)__  | __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. __最も評価値__ が __高い__ 局面になる __合法手__ の中から __ランダム__ に __選択__ する

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

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

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

__評価関数__ で、__`mb`__ の __局面__ で __自分が勝利しているか__ どうかは、__`status` 属性__ が、__自分のマーク__ を __表す値であるか__ どうかで __判定__ することが __できます__ が、__`mb`__ は __相手の手番__ の __局面__ なので、__手番__ を表す __`mb.turn`__ と __`mb.status`__ を __そのまま比較__ しても、__自分__ が __勝利しているか__ どうかを __判定__ することは __できません__。そこで、[以前の記事](https://qiita.com/ysgeso/items/ff28a95dff50322e6b16#ai5-%E3%81%AE%E4%BF%AE%E6%AD%A3)で定義した __`ai5`__ では、下記のプログラムの __7 行目__ のように、__`mb`__ の __`status` 属性__ と、__着手前__ の __自分の手番__ の __局面__ を表す __`mb_orig`__ の、__`turn` 属性__ が __等しいか__ どうかで、その __判定__ を __行っています__。

```python
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`__ の __情報のみ__ を使って __自分__ が __勝利しているか__ どうかを __判定する方法__ について少し考えてみて下さい。

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

```python
def ai_by_score(mb_orig, eval_func, debug=False, rand=True):
略
        score = eval_func(mb_orig, mb)
略
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def ai_by_score(mb_orig, eval_func, debug=False, rand=True):
略
-       score = eval_func(mmb)
+       score = eval_func(mb_orig, mb)
略
```
</details>

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

```python
def eval_func(mb_orig, mb):
    評価関数の処理を記述する
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
-def eval_func(mb):
+def eval_func(mb_orig, mb):
    評価関数の処理を記述する
```
</details>

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

## 判定方法その 1（自分の手番を計算する方法）

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


```python
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`__ を __返す__

```python
 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)
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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)
```
</details>

### 動作の確認

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

```python
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.status`__ が __`Marubatsu.CROSS`__ に __なること__ が __無い__ 点に __注目__ して下さい。この __性質__ から、__現在の局面__ の __手番__ が __〇__ の場合に、__着手後の局面__ で __〇 が勝利__ することを、__下記__ の __条件式__ で __判定__ することができます。

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

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


```python
mb.status == Marubastu.CIRCLE or False
```

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

```python
mb.status == Marubastu.CIRCLE
```

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

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

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

* __3 行目__：__上記の条件式__ で、__自分__ が __勝利__ していることを __判定__ するように __修正__ する

```python
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)
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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)
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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)
```
</details>


### 動作の確認

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

```python
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__ の、__評価関数の中__ で __自分の手番__ を __計算__ する __方法__ は、__自分の手番__ の __情報__ を使って __評価値を計算__ する __評価関数__ を定義する際に、__下記__ のプログラムを __毎回記述__ する __必要__ がある点が __面倒__ です。また、__実際__ に __ルール 6__、__7__ の AI や、その後で __実装する予定__ の __AI__ でも、__評価関数の中__ で __自分の手番__ の __情報__ が __必要__ になります。

```python
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`__ で __初期化__ します。

```python
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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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
```
</details>


### `move` メソッドの修正

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

```python
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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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
```
</details>

### `ai5s` の実装

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

```python
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)
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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)
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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)
```
</details>


### 動作の確認

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

```python
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%
```

## `ai5` と `ai5s` の計算時間の比較

[前回の記事](https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf#%E5%87%A6%E7%90%86%E3%81%AB%E6%99%82%E9%96%93%E3%81%8C%E3%81%8B%E3%81%8B%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%8C%E3%81%82%E3%82%8B) で、「__評価値を利用__ した __アルゴリズム__」は、「__それぞれの条件__ を __順番に判定__ し、__最初__ にみつかった __条件を満たす着手__ を __選択__ する」場合と __比較__ して __処理__ に __時間がかかる場合__ があるという説明を行いました。

そこで、__`ai5`__ と __`ai5s`__ の __処理時間__ を __比較__ することにします。

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

```python
ai_match(ai=[ai5, ai5])
```

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

```python
ai_match(ai=[ai5s, ai5s])
```

[前回の記事](https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf#%E5%87%A6%E7%90%86%E3%81%AB%E6%99%82%E9%96%93%E3%81%8C%E3%81%8B%E3%81%8B%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%8C%E3%81%82%E3%82%8B)では、__`ai1`__ ~ __`ai4`__ と、__対応__ する __`ai1s`__ ~ __`ai4s`__ の __処理時間__ は __約 10 倍以上__ の __差__ がありましたが、__`ai5`__ と __`ai5s`__ の __処理時間__ の __差__ は __数 %__ に過ぎません。その __理由__ は、__`ai1`__ ~ __`ai4`__ が、__`Marubatsu` クラス__ の __インスタンス__ の __深いコピー__ を __作成__ するという、__時間がかかる処理__ を __行っていない__ のに対して、__`ai5`__ は __その処理__ を __行っている__ からです。

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

## `ai5` と `ai5s` が選択する着手の違い

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

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

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

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

```python
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)
```

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

```python
def ai5s(mb, debug=False):
略
    return ai_by_score(mb, eval_func, debug=debug, rand=False)
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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)
```
</details>
:::

## `ai5` と `ai5s` のどちらを選択すべきか

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

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

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

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

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

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

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

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

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

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

```python
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)
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
-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)
```
</details>


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

__`ai6`__ では、最終的に下記のような、正攻法ではない、すこし __ひねった考え方__ で __相手の勝利を阻止__ する __着手__ を __選択__ しました。忘れた方は、[以前の記事](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#ai6-%E3%81%AE%E6%94%B9%E8%89%AF-%E3%81%9D%E3%81%AE-2%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%81%AE%E6%94%B9%E8%89%AF)の説明を見て復習して下さい。

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

残念ながら、__評価値__ を __計算__ する __評価関数__ の __仮引数__ には、__現在の局面__ に対して __合法手__ を __着手__ した __後__ の __局面__ のデータ __しか代入されない__ ので、__現在の局面__ を __相手の手番__ と __みなす__ という __処理__ を行うことは __できません__。また、__上記の考え方__ に __改良__ する __前__ の __[ルール 6 のアルゴリズム](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB-6-%E3%81%AE%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0)__ は、[以前の記事](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#ai6-%E3%81%AE%E6%94%B9%E8%89%AF-%E3%81%9D%E3%81%AE-2%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%81%AE%E6%94%B9%E8%89%AF)で説明したように、__着手の処理__ を行う __数__ が __多くなる__ ため、__処理__ に __大きな時間がかかる__ という __欠点__ があります。

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

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

下記は、現在の局面を相手の手番とみなすという、__改良を行う前__ の __[ルール 6 のアルゴリズム](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB-6-%E3%81%AE%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0)__ の再掲です。この中の、__手順 3__ が __相手の勝利を阻止__ する __着手__ を __選択__ する処理です。

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

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

## ルール 6 改の定義

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

:::note warn
__ルール 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`__ の __実装後__ に、__常にランダムな着手__ を行う __`ai2`__ と __`ai6s`__ で __対戦__ することにします。

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

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

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

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

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

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

表から、__すべての場合__ で、__どちらの方法__ で __着手を選択__ しても、__基本的__ には __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]として、[以前の記事](https://qiita.com/ysgeso/items/a97a3cdf54087e662230#is_same-%E3%81%AE%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0-%E3%81%9D%E3%81%AE-2-%E6%95%B0%E3%82%92%E6%95%B0%E3%81%88%E3%82%8B)で紹介した __下記のプログラム__ の中で __記述__ しました。__`is_same`__ は、[以前の記事](https://qiita.com/ysgeso/items/d457f37df8671e930603#is_winner-%E3%81%AE%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0-%E3%81%9D%E3%81%AE-5-%E5%B7%AE%E5%88%86%E3%81%A7%E5%BA%A7%E6%A8%99%E3%82%92%E8%A8%88%E7%AE%97%E3%81%99%E3%82%8B) で説明した、__差分で座標を計算__ する __アルゴリズム__ を使っているので、忘れた方は復習して下さい。

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

```python
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__ を返す

__それぞれ__ の __数を数える処理__ は、[以前の記事](https://qiita.com/ysgeso/items/5725504c97a4490fbadb#defaultdict-%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E9%9B%86%E8%A8%88)で紹介した、__defaultdict__ を __利用した集計__ と __同じ方法__ で行うことができるので、__`count_marks`__ は下記のプログラムのように定義できます。

* __5 行目__：__〇__、__×__、__空のマス__ を __数える__ ための __ローカル変数 `count`__ を、__既定値__ が __`0`__ の __defaultdict__ で __初期化__ する
* __7 行目__：__`count`__ の、__(x, y)__ のマスの __マーク__ を __キー__ とする __キーの値__ を __1 増やす__
* __11 行目__：__`count`__ を __返り値__ として __返す__

```python
 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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

<details><summary><strong>修正箇所（<code>is_same</code> との違いです）</strong></summary>

```diff_python
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
```
</details>

下記は、__いくつかの局面__ に対して、__一番上の行__ の __3 マス__ に __配置__ された __〇__、__×__、__空のマス__ の __数__ を、__`count_marks`__ で __数えた結果__ を __表示__ するプログラムです。__`count_marks`__ に記述した __実引数__ の __意味が分からない__ 方は、[以前の記事](https://qiita.com/ysgeso/items/d457f37df8671e930603#%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%81%AE%E8%80%83%E3%81%88%E6%96%B9)を復習して下さい。__実行結果__ から、__正しく数えている__ ことが __確認__ できます。

```python
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})
```

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

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

## `ai6s` の定義

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

```python
    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`__ を __参考__ に、下記のプログラムのように記述できます。ただし、下記の __11__、__14__、__18__、__22 行目__ の「__次の相手の局面で相手が勝利できる__」の __条件式__ は __未完成__ です。その __条件式__ を __どのように記述__ すればよいかについて少し考えてみて下さい。

```python
 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`__ に __代入__ されています。従って、下記の __条件式__ で __判定__ できます。

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

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

```python
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)        
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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)      
```
</details>


## 動作の確認

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

```python
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%
```

## `ai6` と `ai6s` が選択する着手の違い

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

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

```python
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)
```

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

## `ai2` との対戦

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

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

| 関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 | 欠陥 |
|:-:|--:|--:|--:|--:|--:|--:|--:|--:|--:|:-:|
| __[`ai6`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-6%E7%9B%B8%E6%89%8B%E3%81%AE%E5%8B%9D%E5%88%A9%E3%82%92%E9%98%BB%E6%AD%A2%E3%81%99%E3%82%8B)__  | __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`  |

なお、[以前の記事](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB-3-%E3%81%A8-%E3%83%AB%E3%83%BC%E3%83%AB-6-%E3%81%AE%E9%A0%86%E7%95%AA) で示したように、__ルール 7__ で __着手__ を __行う__ 場合に、__真ん中__ のマスに __着手__ した __時点__ で __自分が勝利__ することは __あり得ない__ ので、下記の表のように、「__真ん中のマスに着手__ する」局面と、「__自分が勝利している__」局面の __評価値__ を __同じ__ に __設定__ しても __構いません__。なお、本記事では上記の表で評価値を設定することにします。

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

## `ai7s` の定義

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

* __4、5 行目__：__真ん中__ のマスに __着手__ した場合は、__評価値__ として __`2`__  を __返す__ 処理を __追加__ する

```python
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 と同じなので省略       
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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)      
```
</details>

<details><summary><strong>修正箇所</strong></summary>

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

以下は ai6s と同じなので省略
```
</details>

なお、__[`ai7`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-7%E3%83%AB%E3%83%BC%E3%83%AB3-%E3%81%A8-%E3%83%AB%E3%83%BC%E3%83%AB-6-%E3%81%AE%E4%BD%B5%E7%94%A8)__ では、その __ブロックの中__ で __`ai6` を呼び出す__ ことで、__簡潔__ なプログラムで __定義__ することが __できました__ が、__`ai6s`__ の __評価関数__ は、__ローカル関数__ として __定義__ しているので、__`ai7s`__ の __ブロックの中__ で、__`ai6s`__ の __評価関数__ を __利用__ することは __できません__。そのため、__`ai7s`__ を __`ai7`__ のように __簡潔に記述__ することは __できません__。

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

## 動作の確認

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

```python
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__ ~ __7__ の __AI__ を __評価値を利用__ した __アルゴリズム__ で定義しました。その際に、__ルール 6__、__7__ の __条件__ の一つを __言い換えた__、__ルール 6 改__、__7 改__ を __定義__ しました。

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

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

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

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

https://github.com/ysgeso/marubatsu/blob/master/050/marubatsu.ipynb

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

https://github.com/ysgeso/marubatsu/blob/master/050/marubatsu_new.py

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

https://github.com/ysgeso/marubatsu/blob/master/050/ai_new.py

# 次回の記事

https://qiita.com/ysgeso/items/58754ba98c286f414ea4

[^1]:実際には、__途中__ で __着手を選択__ する __場合があり__、その場合はすべての着手を行う必要はありませんが、__多くの場合__ で __すべての着手__ を __行う必要__ があります
[^2]:実際の `Marubatsu` クラスの `is_same` メソッドは、これとは[別の方法](https://qiita.com/ysgeso/items/a97a3cdf54087e662230#is_same-%E3%81%AE%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0-%E3%81%9D%E3%81%AE-5--%E6%BC%94%E7%AE%97%E5%AD%90%E3%81%AE%E5%88%A9%E7%94%A8)で実装しています。