Pythonで〇×ゲームのAIを一から作成する　その132　デコレーター式を利用したすべての AI の関数の再定義

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/b4703c69e6211faa089c

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

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

| リンク | 説明 |
|:--|:--|
| [marubatsu.py](https://github.com/ysgeso/marubatsu/blob/master/132/marubatsu.py)  | Marubatsu、Marubatsu_GUI クラスの定義|
| [ai.py](https://github.com/ysgeso/marubatsu/blob/master/132/ai.py)  |  AI に関する関数 |
| [test.py](https://github.com/ysgeso/marubatsu/blob/master/132/test.py)  |  テストに関する関数 |
|  [util.py](https://github.com/ysgeso/marubatsu/blob/master/132/util.py) |  ユーティリティ関数の定義。現在は `gui_play` のみ定義されている |
| [tree.py](https://github.com/ysgeso/marubatsu/blob/master/132/tree.py)  | ゲーム木に関する Node、Mbtree クラスの定義  |
| [gui.py](https://github.com/ysgeso/marubatsu/blob/master/132/gui.py)  |  GUI に関する処理を行う基底クラスとなる GUI クラスの定義  |

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

https://qiita.com/ysgeso/items/10d1d01192c014173e4b

# デコレーター式を利用したすべての AI の関数の再定義

前回までの記事で、デコレーター式を利用した `ai2s` と `ai11s` の定義を行いました。今回の記事では __すべての AI の関数__ を __デコレーター式を利用して定義し直す__ ことにします。

## `ai2s` の修正

[前回の記事](https://qiita.com/ysgeso/items/b4703c69e6211faa089c#ai_by_score-%E3%81%AE%E4%BF%AE%E6%AD%A3)で、ラッパー関数の中で、__ラップする関数を呼び出す処理__ を、下記のプログラムの 4 行目のように、__実引数に `mb` と `debug` を記述して呼び出す__ ように修正しました。

```python
1  def ai_by_score(eval_func):
2      @wraps(eval_func)
3      def wrapper(mb_orig, debug=False, *args, rand=True, analyze=False, **kwargs):
省略
4              score = eval_func(mb, debug, *args, **kwargs)
省略
5      return wrapper
```

そのため、__`@ai_by_score`__ というデコレーター式の __下に記述する関数の定義__ では、__必ず仮引数として `mb` と `debug` を持つ必要__ があります。

しかし、[以前の記事](https://qiita.com/ysgeso/items/7e1d948c4b9208ab44fb#%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC%E5%BC%8F%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F-ai2s-%E3%81%AE%E5%AE%9A%E7%BE%A9)で `ai2s` をデコレーター式で定義した際は、`ai_by_score` を上記のように __修正する前__ であったため、下記のプログラムの 2 行目のように、__仮引数には `mb` のみが記述__ されています。

```python
@ai_by_score
def ai2s(mb):
    return 0
```

そのため、下記のプログラムで __`ai2s`__ を呼び出すと実行結果のように、__エラーが発生__ します。エラーメッセージは、`ai2s` に対して 1 つの位置引数を記述する必要があるのに対して、2 つの位置引数が記述されているという意味の表示が行われています。

```python
from ai import ai2s
from marubatsu import Marubatsu

mb = Marubatsu()
print(ai2s(mb))
```

実行結果

```
略
File c:\Users\ys\ai\marubatsu\132\ai.py:169, in ai_by_score.<locals>.wrapper(mb_orig, debug, rand, analyze, *args, **kwargs)
    166 mb.move(x, y)
    167 dprint(debug, mb)
--> 169 score = eval_func(mb, debug, *args, **kwargs)
    170 dprint(debug, "score", score, "best score", best_score)
    171 if analyze:

TypeError: ai2s() takes 1 positional argument but 2 were given
```

__`ai2s`__ を下記のプログラムのように、__`mb` と `debug` の 2 つの仮引数を持つ__ ように定義する事で、この問題を解決することができます。下記のプログラムの 4、5 行目のラップする関数のように、__`debug` の値を利用しない__ 場合でも __仮引数 `debug` を必ず記述__ する必要があります。

* __2 行目__：仮引数 `debug=False` を追加する

```python
1  from ai import ai_by_score
2
3  @ai_by_score
4  def ai2s(mb, debug=False):
5      return 0
```

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

```python
from ai import ai_by_score

@ai_by_score
def ai2s(mb, debug=False):
    return 0
```
</details>

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

```diff_python
from ai import ai_by_score

@ai_by_score
-def ai2s(mb):
+def ai2s(mb, debug=False):
    return 0
```
</details>

上記の修正後に下記のプログラムを実行すると、エラーが発生しなくなったことが確認できます。

```python
print(ai2s(mb))
```

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

```
(2, 1)
```

### `ai2s` の仮引数に関する補足

下記のプログラムのように、__ラップする関数の仮引数__ に `debug` の代わりに __`*args` と `**kwargs` を記述__ するという方法もありますが、__この方法はお勧めしません__。なお、`ai2s` と区別するため、下記の関数の名前を `ai2s_2` としました。

```python
1  @ai_by_score
2  def ai2s_2(mb, *args, **kwargs):
3      return 0
```

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

```python
@ai_by_score
def ai2s_2(mb, *args, **kwargs):
    return 0
```
</details>

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

```diff_python
@ai_by_score
-def ai2s(mb, debug=False):
+def ai2s_2(mb, *args, **kwargs):
    return 0
```
</details>

この方法で `ai2s_2` を定義する事で、下記のプログラムのように __`ai2s_2`__ を呼び出す際に、__2 つ目以降の実引数__ に __どのような値を記述__ して呼び出しても __エラーが発生しなくなります__。なお、2 つ目の実引数に `False` を記述しているのは、ラッパー関数の仮引数 `debug` に `False` を代入するためです。

```python
ai2s_2(mb, False, 1, 2, a=3, b=4, c=5)
```

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

```
(1, 1)
```

__どのような実引数を記述してもエラーにならないこと__ は、メリットだと思う人がいるかもしれませんが、これは __大きなデメリット__ になります。

例えば、__`ai2s`__ では、下記のプログラムのように、__キーワード引数 `debug` の綴り__ を __`debig` のように間違って記述__ して呼び出すと、実行結果のように予期しないキーワード引数が記述されたことが原因の __エラーが発生__ します。エラーが発生することで、__プログラムが停止する__ ため、__何かがおかしいことに必ず気づくことができます__。

```python
print(ai2s(mb, debig=True))
```

実行結果

```
略
File c:\Users\ys\ai\marubatsu\132\ai.py:169, in ai_by_score.<locals>.wrapper(mb_orig, debug, rand, analyze, *args, **kwargs)
    166 mb.move(x, y)
    167 dprint(debug, mb)
--> 169 score = eval_func(mb, debug, *args, **kwargs)
    170 dprint(debug, "score", score, "best score", best_score)
    171 if analyze:

TypeError: ai2s() got an unexpected keyword argument 'debig'
```

一方で、下記のプログラムのように、同じ __間違ったキーワード引数を記述__ して __`ai2s_s` を呼び出しても__ 実行結果のように __エラーは発生しません__ が、__本来行いたかった__ `debug=True` を記述した際の __デバッグ表示も行われません__。エラーが発生しないため、__プログラムの処理が続行されるため__、__間違った処理__ が行われていることに __気が付かない可能性が高くなります__。

```python
print(ai2s_2(mb, debig=True))
```

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

```
(1, 2)
```

:::note warn
このように、__必要のない実引数を記述してもエラーにならないこと__ は、メリットよりも __デメリットの方が多い__ ため、__必要がない場合__ に仮引数に `*args` と `**kwargs` を __記述しないほうが良い__ でしょう。

__`*args` や `**kwargs` が必要となる具体例__ としては以下のような例が挙げられます。

* __任意の仮引数を持つ関数__ に対する __ラッパー関数__
* `print` のように、__任意の数の実引数__ に対して処理を行う関数
:::

## `ai1s` の修正

__`ai1s`__ は [ルール 1](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#%E6%9C%80%E5%88%9D%E3%81%AB%E8%A6%8B%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%9F%E7%A9%BA%E3%81%84%E3%81%A6%E3%81%84%E3%82%8B%E3%83%9E%E3%82%B9%E3%81%AB%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%86-ai)の、「左上から順に空いているマスを探し、最初に見つかったマスに着手 する」という方法で着手を選択しますが、下記のプログラムのように評価値を計算する __`eval_func` は常に `0` を返し__、`ai_by_score` の実引数に __`rand=False` を記述__ することで、乱数を使わずに __最初の合法手を選択する__ という、特殊な処理を行っています。

```python
def ai1s(mb, debug=False):
    def eval_func(mb):
        return 0

    return ai_by_score(mb, eval_func, debug=debug, rand=False)
```

そのため、__上記の `eval_func`__ に対して `@ai_by_score` のデコレーター式を記述して __`ai1s` を定義__ すると、ランダムな着手を行う __`ai2s` と同じ処理を行う関数が定義__ されてしまします。

__`@ai_by_score`__ のデコレーター式を利用して __ルール 1 の `ai1s` を定義__ するためには、__直前の着手__ に対して、__左上のマスから__ 右方向に __順番に小さくなっていく__ ような評価値を計算する関数を定義する必要があります。具体的な評価値として、本記事では下記のプログラムのように、__直前の着手__ に対して __左上のマスから順に `8`、`7`、・・・、`0`__ という評価値を計算する関数を定義することにします。

* __3、4 行目__：ゲーム開始時の局面は、直前の着手が存在しないので、その場合は評価値として `0` を返すようにする
* __5 ~ 7 行目__：直前の着手を (x, y) とすると __`x + y * 3`__ という式で、[以前の記事](https://qiita.com/ysgeso/items/1a8a4319ef3c896ef271#%E6%95%B0%E5%80%A4%E5%BA%A7%E6%A8%99)で説明した、下図のように左上のマスから順に `0` ~ `8` が割り当てられた __数値座標__ を計算できる。__`8` から数値座標を引き算する__ ことで、求める評価値を計算することができる


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/8930ae16-efc9-e09b-c759-6b8e367305e3.png" width="150">


```python
1  @ai_by_score
2  def ai1s(mb, debug=False):
3      if mb.last_move is None:
4          return 0
5      else:
6          x, y = mb.last_move
7          return 8 - (x + y * 3)
```

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

```python
@ai_by_score
def ai1s(mb, debug=False):
    if mb.last_move is None:
        return 0
    else:
        x, y = mb.last_move
        return 8 - (x + y * 3)
```
</details>

上記の定義後に下記のプログラムを実行すると、ゲーム開始時の局面に対して、`ai1s` が左上の (0, 0) を選択することが確認できます。

```python
print(ai1s(mb))
```

実行結果

```
(0, 0)
```

また、下記のプログラムで実引数に __`analyze=True`__ を記述して `ai1s` を呼び出すと、実行結果のように __左上のマスから順に `8`、`7`、・・・、`0` という評価値が計算されている__ ことが確認できます。なお、__`pprint`__ は __dict の要素__ を登録した順ではなく、__キーの値によって並べ替えて表示する__ ので、下記のようにキーの値が __左上のマスから縦方向の順番__ で表示され、実行結果では評価値が 8、5、2・・・の順で表示されます。

```python
from pprint import pprint

pprint(ai1s(mb, analyze=True))
```

実行結果

```
{'candidate': [(0, 0)],
 'score_by_move': {(0, 0): 8,
                   (0, 1): 5,
                   (0, 2): 2,
                   (1, 0): 7,
                   (1, 1): 4,
                   (1, 2): 1,
                   (2, 0): 6,
                   (2, 1): 3,
                   (2, 2): 0}}
```

下記のプログラムのように __`print` で表示__ すれば __登録した順__ で dict の要素が表示されます。

```python
print(ai1s(mb, analyze=True))
```

実行結果

```
{'candidate': [(0, 0)], 'score_by_move': {(0, 0): 8, (1, 0): 7, (2, 0): 6, (0, 1): 5, (1, 1): 4, (2, 1): 3, (0, 2): 2, (1, 2): 1, (2, 2): 0}}
```

## 残りの評価値を利用して着手を選択する AI の再定義

ここまでで、評価値を利用して着手を選択する AI のうち、`ai1s`、`ai2s`、`ai11s` をデコレーター式を利用して定義しなおしました。__残り__ の、`ai3s` ~ `ai10s`、`ai10s` ~ `ai14s` に対しても __同様の方法__ で __`@ai_by_score`__ のデコレーター式を __利用して定義し直す__ ことができます。

長くなるので折りたたみますが、下記のプログラムが `ai1s` ~ `ai14s` を `@ai_by_score` のデコレーター式を利用して定義しなおしたものです。

<details><summary><strong><code>ai1s</code> ~ <code>ai14s</code> の再定義</strong></summary>

```python
from marubatsu import Markpat

@ai_by_score
def ai1s(mb, debug=False):
    if mb.last_move is None:
        return 0
    else:
        x, y = mb.last_move
        return 8 - (x + y * 3)

@ai_by_score
def ai2s(mb, debug=False):
    return 0    
    
@ai_by_score
def ai3s(mb, debug=False):
    if mb.last_move == (1, 1):
        return 1
    else:
        return 0
    
@ai_by_score
def ai4s(mb, debug=False):
    x, y = mb.last_move
    if mb.last_move == (1, 1):
        return 10
    elif x % 2 == 0 and y % 2 == 0:
        return 9 - (x + y * 3)
    else:
        return 0
    
@ai_by_score
def ai5s(mb, debug=False):
    if mb.status == mb.last_turn:
        return 1
    else:
        return 0

@ai_by_score
def ai6s(mb, debug=False):
    # 自分が勝利している場合は、評価値として 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

@ai_by_score
def ai7s(mb, debug=False):
    # 真ん中のマスに着手している場合は、評価値として 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

@ai_by_score
def ai8s(mb, debug=False):
    # 真ん中のマスに着手している場合は、評価値として 3 を返す
    if mb.last_move == (1, 1):
        return 3

    # 自分が勝利している場合は、評価値として 2 を返す
    if mb.status == mb.last_turn:
        return 2

    markpats = mb.enum_markpats()
    # 相手が勝利できる場合は評価値として -1 を返す
    if Markpat(last_turn=0, turn=2, empty=1) in markpats:
        return -1
    # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
    elif Markpat(last_turn=2, turn=0, empty=1) in markpats:
        return 1
    # それ以外の場合は評価値として 0 を返す
    else:
        return 0
    
@ai_by_score
def ai9s(mb, debug=False):
    # 真ん中のマスに着手している場合は、評価値として 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
    
@ai_by_score
def ai10s(mb, debug=False):
    # 真ん中のマスに着手している場合は、評価値として 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

@ai_by_score
def ai11s(mb, debug=False, score_201=2, score_102=0.5, score_012=-1):
    # 真ん中のマスに着手している場合は、評価値として 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        
    # 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
    if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
        score += score_201
    # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
    score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
    # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
    score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
    
    # 計算した評価値を返す
    return score

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

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

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

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

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

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

@ai_by_score
def ai14s(mb, debug=False, score_victory=300, score_sure_victory=200, \
          score_defeat=-100, score_special=100, score_201=2, \
          score_102=0.5, score_012=-1):
    # 評価値の合計を計算する変数を 0 で初期化する
    score = 0        

    # 自分が勝利している場合
    if mb.status == mb.last_turn:
        return score_victory

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

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

なお、再定義した AI の関数が __正しく動作するか__ どうかの __検証は次回の記事__ で行います。

# 評価値を計算しない AI の関数に対するデコレーター式による再定義

下記のプログラムの __`ai2`__ などの、__評価値を計算せずに着手の選択を行う AI の関数__ に対しては、__`ai_by_score` を利用__ してラッパー関数を __定義する事はできません__。

```python
def ai2(mb):
    legal_moves = mb.calc_legal_moves()
    return choice(legal_moves)
```

__評価値を計算しない AI に対するラッパー関数__ は、`ai_by_score` と同様に、ラップする AI の関数に対して、__下記の機能を追加する必要__ があります。

* デバッグ表示を行うことを表す仮引数 `debug` を追加する
* ランダムな着手を行うかどうかを表す仮引数 `rand` を追加する
* 候補手の一覧[^1]を返り値として返すことを表す仮引数 `analyze` を追加する

この中で、__仮引数 `analyze` を追加__ するという __機能の拡張__ は、[以前の記事](https://qiita.com/ysgeso/items/2c76474ff139a824f4fd#%E4%BB%AE%E5%BC%95%E6%95%B0-analyze-%E3%81%AE%E8%BF%BD%E5%8A%A0)で下記の __`ai3` に対して既に行っています__。

```python
def ai3(mb):
    if mb.board[1][1] == Marubatsu.EMPTY:
        return 1, 1
    legal_moves = mb.calc_legal_moves()
    return choice(legal_moves)
```

上記のプログラムに対して、__下記のように修正__ することで、仮引数 `analyze` に代入された値によって候補手の一覧を返り値として返すように __`ai3` の機能を拡張__ しています。

```python
 1  def ai3(mb, analyze=False):
 2      if mb.board[1][1] == Marubatsu.EMPTY:
 3          candidate = [(1, 1)]
 4      else:
 5          candidate = mb.calc_legal_moves()
 6      if analyze:
 7          return {
 8              "candidate": candidate,
 9              "score_by_move": None
10          }
11      else:
12          return choice(candidate)
```

そこで、__上記の修正を参考__ に、最初に __仮引数 `analyze` に関する機能を追加__ したラッパー関数を作成する __デコレーターの関数を定義__ する事にし、その後で仮引数 `debug` と `rand` の機能を追加したデコレーターの関数を定義する事にします。

## 仮引数 `analyze` の機能の追加

上記のプログラムの __6 ~ 12 行目__ が `ai3` に対して、__仮引数 `analyze` の処理を追加した部分__ です。従って、ラッパー関数を定義して `ai3` の機能の拡張を行う場合は、__この部分の処理をラッパー関数の中に記述__ します。

具体的には、デコレータの関数を以下のように定義します。なお、デコレーターの関数の名前を `ai_by_candidate` とした理由はこの後で説明します。

* __6 行目__：仮引数 `mb`、`*args`、`analyze`、`**args` を持つラッパー関数を定義する。ラップする関数が任意の仮引数を持つことができるように [仮引数 `*args` と `**kwargs` を記述](https://qiita.com/ysgeso/items/b4703c69e6211faa089c#mb-%E4%BB%A5%E5%A4%96%E3%81%AE%E4%BB%AE%E5%BC%95%E6%95%B0%E3%82%92%E6%8C%81%E3%81%A4-ai-%E3%81%AE%E9%96%A2%E6%95%B0%E3%81%AE%E3%83%A9%E3%83%83%E3%83%91%E3%83%BC%E9%96%A2%E6%95%B0) する。また、仮引数 `analyze` は [ラッパー関数のみで使われる仮引数](https://qiita.com/ysgeso/items/b4703c69e6211faa089c#ai_by_score-%E3%81%AE%E4%BF%AE%E6%AD%A3) ので、`*args` より後で記述する
* __7 行目__：ラップする関数を呼び出し、返り値を `candidate` に代入する
* __8 ~ 14 行目__：`ai3` の機能を拡張する、上記の 6 ~ 12 行目と同じ処理を行う

```python
 1  from functools import wraps
 2  from random import choice
 3
 4  def ai_by_candidate(func):
 5      @wraps(eval_func)
 6      def wrapper(mb, *args, analyze=False, **kwargs):
 7          candidate = func(mb, *args, **kwargs)
 8          if analyze:
 9              return {
10                  "candidate": candidate,
11                  "score_by_move": None
12              }
13          else:
14              return choice(candidate)
15       
16      return wrapper
```

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

```python
from functools import wraps
from random import choice

def ai_by_candidate(func):
    @wraps(eval_func)
    def wrapper(mb, *args, analyze=False, **kwargs):
        candidate = func(mb, *args, **kwargs)
        if analyze:
            return {
                "candidate": candidate,
                "score_by_move": None
            }
        else:
            return choice(candidate)
        
    return wrapper
```
</details>

__`@ai_by_candidate`__ のデコレーター式を利用して、__`ai3`__ を下記のプログラムのように定義し直すことができます。上記のプログラムの __ラッパー関数の 6 行目__ で __ラップする関数を呼び出し__、__返り値として候補手の一覧を受け取っている__ ので、下記のプログラムのように 7 行目で __候補手の一覧を表す `candidate` を返す処理を記述__ する必要があります。

```python
1  @ai_by_candidate
2  def ai3(mb):
3      if mb.board[1][1] == Marubatsu.EMPTY:
4          candidate = [(1, 1)]
5      else:
6          candidate = mb.calc_legal_moves()
7      return candidate
```

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

```python
@ai_by_candidate
def ai3(mb):
    if mb.board[1][1] == Marubatsu.EMPTY:
        candidate = [(1, 1)]
    else:
        candidate = mb.calc_legal_moves()
    return candidate
```
</details>

上記の実行後に下記のプログラムを実行することで、実行結果から `ai3` が正しく動作することが確認できます。なお、__`ai3`__ は __真ん中のマスに優先的に着手を行う [ルール 3](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#%E3%83%AB%E3%83%BC%E3%83%AB-3-%E7%9C%9F%E3%82%93%E4%B8%AD%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)__ を実装した AI なので、__ゲーム開始時の局面では必ず (1, 1) に着手__ を行います。

```python
print(ai3(mb))
```

実行結果

```
(1, 1)
```

また、下記のプログラムで実引数に __`analyze=True`__ を記述して `ai3` を呼び出した場合も、実行結果から正しく動作することが確認できます。

```python
print(ai3(mb, analyze=True))
```

実行結果

```
{'candidate': [(1, 1)], 'score_by_move': None}
```

### `@ai_by_candidate` を利用した `ai2` の定義

__`@ai_by_candidate`__ を利用した `ai2` を定義する際に、下記のプログラムのように __元の `ai2` の定義をそのまま記述__ することは __できません__。

```python
@ai_by_candidate
def ai2(mb):
    legal_moves = mb.calc_legal_moves()
    return choice(legal_moves)
```

上記の実行後に下記のプログラムを実行すると、実行結果のように座標ではなく、__`0` などの数値が表示__ されます。なお、__表示される数値は 0、1、2 の中からランダムに選ばれます__。

```python
print(ai2(mb))
```

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

```
0
```

このようなことが起きるのは、__`ai_by_candidate`__ が定義する __ラッパー関数__ が、__ラップする関数の返り値__ として __候補手の一覧を必要とする__ からです。上記の __`ai2`__ の __ラップする関数__ は、候補手の一覧ではなく、候補手の中からランダムに選択した __1 つの合法手を返します__。例えば __`(0, 1)`__ という合法手が __返り値として得られた場合__ は、`ai_by_candidate` の `wrapper` の中で __`choice((0, 1))`__ が返り値として返されるので、__`0` または `1`__ の __いずれかの数値が返ります__。このように、上記の `ai2` は、__合法手__ の __x 座標または y 座標のどちらかを返り値として返す__ という間違った処理を行います。

従って、__`ai_by_candidate`__ をデコレーターとして利用する場合は、__ラップする関数__ が返り値として __候補手の一覧のデータを返す__ 必要があります。下記は、そのように `ai2` を定義したプログラムです。なお、__デコレーターの関数の名前__ を __`ai_by_candidate`__ としたのは、__候補手（candidate）を返す関数に対する AI のラッパー関数を作成__ する処理を行うからです。

```python
@ai_by_candidate
def ai2(mb):
    return mb.calc_legal_moves()
```

上記のように `ai2` を修正することで、下記のプログラムのように `ai2` が正しい処理を行うようになったことが確認できます。

```python
print(ai2(mb))
```

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

```
(1, 1)
```

## 仮引数 `debug` と `rand` の機能の追加

次に、__`ai_by_candidate`__ 対して __仮引数 `debug` と `rand` の機能を追加__ することにします。__`debug` に `True` が代入__ されていた場合は、__候補手の一覧を表示__ することにします。

下記は、そのように `ai_by_candidate` を修正したプログラムです。

* __5 行目__：ラッパー関数の仮引数 `*args` の前に仮引数 `debug` を、後に `rand` を追加する
* __6 行目__：`ay_by_score` の場合と同様に、ラップする関数を呼び出す際に、2 つ目の実引数に `debug` を記述するように修正する
* __7 行目__：[`dprint`](https://qiita.com/ysgeso/items/474db9ea1805993eaa34#%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E8%A1%A8%E7%A4%BA%E3%82%92%E8%A1%8C%E3%81%86%E9%96%A2%E6%95%B0%E3%81%AE%E5%AE%9A%E7%BE%A9) を使って、`debug` が `True` の場合に候補手の一覧を表示する
* __14 ~ 17 行目__：`rand` が `True` の場合は候補手の中からランダムに選択した合法手を返し、`False` の場合は先頭の候補手を返すように修正する

```python
 1  from ai import dprint
 2
 3  def ai_by_candidate(func):
 4      @wraps(func)
 5      def wrapper(mb, debug=False, *args, rand=True, analyze=False, **kwargs):
 6          candidate = func(mb, debug, *args, **kwargs)
 7          dprint(debug, "candidate", candidate)
 8          if analyze:
 9              return {
10                  "candidate": candidate,
11                  "score_by_move": None
12              }
13          else:
14              if rand:
15                  return choice(candidate)
16              else:
17                  return candidate[0]
18        
19      return wrapper
```

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

```python
from functools import wraps
from ai import dprint

def ai_by_candidate(func):
    @wraps(func)
    def wrapper(mb, debug=False, *args, rand=True, analyze=False, **kwargs):
        candidate = func(mb, debug, *args, **kwargs)
        dprint(debug, "candidate", candidate)
        if analyze:
            return {
                "candidate": candidate,
                "score_by_move": None
            }
        else:
            if rand:
                return choice(candidate)
            else:
                return candidate[0]
        
    return wrapper
```
</details>

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

```diff_python
from ai import dprint

def ai_by_candidate(func):
    @wraps(func)
-   def wrapper(mb, *args, analyze=False, **kwargs):
+   def wrapper(mb, debug=False, *args, rand=True, analyze=False, **kwargs):
-       candidate = func(mb, *args, **kwargs)
+       candidate = func(mb, debug, *args, **kwargs)
+       dprint(debug, "candidate", candidate)
        if analyze:
            return {
                "candidate": candidate,
                "score_by_move": None
            }
        else:
-           return choice(candidate)
+           if rand:
+               return choice(candidate)
+           else:
+               return candidate[0]
        
    return wrapper
```
</details>

上記の修正後に、下記のプログラムで `ai2` を定義し直します。__ラップする関数を呼び出す際に 2 つ目の実引数に `debug` を記述する__ ように修正したので、__2 行目では仮引数 `debug` を追加する__ 必要があります。

```python
1  @ai_by_candidate
2  def ai2(mb, debug=False):
3      return mb.calc_legal_moves()
```

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

```python
@ai_by_candidate
def ai2(mb, debug=False):
    return mb.calc_legal_moves()
```
</details>

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

```diff_python
@ai_by_candidate
-def ai2(mb):
+def ai2(mb, debug=False):
    return mb.calc_legal_moves()
```
</details>

上記の修正後に下記のプログラムで __`debug=True`__ と __`rand=False`__ を記述して `ai2` を呼び出すと、実行結果のように __候補手の一覧が表示__ され、__必ず最初の候補手である (0, 0)__ が返り値として返るようになります。何度か下記のプログラムを実行して常に (0, 0) が返るようになったことを確認して下さい。

```python
print(ai2(mb, rand=False, debug=True))
```

実行結果

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

## 残りの評価値を利用しない AI の再定義

長くなるので折りたたみますが、下記のプログラムが `ai1` ~ `ai7`、`ai_gt1` ~ `ai_gt6` を `@ai_by_candidate` のデコレーター式を利用して定義しなおしたものです。

その際に、先ほど説明したように、修正前の関数に対して、__返り値として候補手の一覧を返す__ ように修正する必要があります。また、元のプログラムが __`return 1, 1`__ のように、__一つの合法手を返す場合__ は、__`return [(1, 1)]`__ のように、合法手を 1 つだけ持つ __list の形式で返す__ ように修正する必要がある点に注意して下さい。

<details><summary><strong><code>ai1</code> ~ <code>ai7</code>、<code>ai_gt1</code> ~ <code>ai_gt6</code> の再定義</strong></summary>

```python
from copy import deepcopy

@ai_by_candidate
def ai1(mb, debug=False):
    for y in range(mb.BOARD_SIZE):
        for x in range(mb.BOARD_SIZE):
            if mb.board[x][y] == Marubatsu.EMPTY:
               return [(x, y)]
           
@ai_by_candidate
def ai2(mb, debug=False):
    legal_moves = mb.calc_legal_moves()
    return legal_moves

@ai_by_candidate
def ai3(mb, debug=False):
    if mb.board[1][1] == Marubatsu.EMPTY:
        candidate = [(1, 1)]
    else:
        candidate = mb.calc_legal_moves()
    return candidate

@ai_by_candidate
def ai4(mb, debug=False):
    if mb.board[1][1] == Marubatsu.EMPTY:
        return [(1, 1)]
    for y in range(0, 3, 2):
        for x in range(0, 3, 2):
            if mb.board[x][y] == Marubatsu.EMPTY:
                return [(x, y)]
    return mb.calc_legal_moves()

@ai_by_candidate
def ai5(mb_orig, debug=False):
    legal_moves = mb_orig.calc_legal_moves()
    # すべての合法手について繰り返し処理を行う
    for move in legal_moves:
        # mb_orig をコピーし、コピーしたもの対して着手を行う
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        # 勝利していれば、その合法手を返り値として返す
        if mb.status == mb_orig.turn:
            return [move]
    return legal_moves

@ai_by_candidate
def ai6(mb_orig, debug=False):
    # mb_orig の合法手の中で、自分が勝利できる合法手があればそこに着手する
    legal_moves = mb_orig.calc_legal_moves()
    # 合法手が 1 つしかない場合は、その合法手を返り値として返す
    if len(legal_moves) == 1:
        return legal_moves
    # 合法手の中で、勝てるマスがあれば、その合法手を返り値として返す
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        if mb.status == mb_orig.turn:
            return [move]
    # 〇 が勝利する合法手が存在しないことが確定した場合は、
    # 現在の局面を相手の手番とみなし、合法手の中で、相手が着手して
    # 勝利するマスがあれば、その合法手を返り値として返す
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        # 現在の局面の手番を入れ替える
        mb.turn = Marubatsu.CROSS if mb.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
        enemy_turn = mb.turn
        x, y = move
        mb.move(x, y)
        if mb.status == enemy_turn:
            return [move]
       
    return legal_moves

@ai_by_candidate
def ai7(mb, debug=False):
    if mb.board[1][1] == Marubatsu.EMPTY:
        return [(1, 1)]
    return ai6(mb, debug=debug, analyze=True)["candidate"]

@ai_by_candidate
def ai_gt1(mb, debug=False, mbtree=None):
    node = mbtree.root
    for move in mb.records[1:]:
        node = node.children_by_move[move]

    bestmoves = []
    for move, childnode in node.children_by_move.items():
        if node.score == childnode.score:
            bestmoves.append(move)
    return bestmoves
    
@ai_by_candidate
def ai_gt2(mb, debug=False, mbtree=None):
    node = mbtree.root
    for move in mb.records[1:]:
        node = node.children_by_move[move]

    return node.bestmoves

@ai_by_candidate
def ai_gt3(mb, debug=False, mbtree=None):
    node = mbtree.nodelist_by_mb[tuple(mb.records)]
    return node.bestmoves    

@ai_by_candidate
def ai_gt4(mb, debug=False, mbtree=None):
    return mbtree.bestmoves_by_mb[tuple(mb.records)]

@ai_by_candidate
def ai_gt5(mb, debug=False, bestmoves=None):
    return bestmoves[tuple(mb.records)]

@ai_by_candidate
def ai_gt6(mb, debug=False, bestmoves_by_board=None):
    return bestmoves_by_board[mb.board_to_str()]    
```
</details>

いくつかの関数の再定義について補足します。

### `ai7` の再定義

修正前の __`ai7`__ は下記のプログラムのように、その中で __`ai6` を呼び出した返り値を返しています__ が、__`ai6` の返り値は候補手の一覧ではない__ ので、この関数の前に __`@ai_by_candidate` のデコレーター式を記述__ しても __うまくいきません__。

```python
def ai7(mb):
    if mb.board[1][1] == Marubatsu.EMPTY:
        return 1, 1
    return ai6(mb)
```

そのため、__`@ai_by_candidate`__ のデコレーター式で __`ai7` を定義し直す__ 場合は、下記のプログラムの 5 行目のように __候補手の一覧を返すように修正__ する必要があります。

* __5 行目__：実引数に __`analyze=True`__ を記述して `ai6` を呼び出すことで、__`ai6` の返り値の中に候補手の一覧が含まれる__ ように修正する。また、その __返り値の dict__ の __`candidate`__ というキーの値に __候補手の一覧が代入__ されているので、__そのキーの値を返り値として返す__ ように修正する

```python
1  @ai_by_candidate
2  def ai7(mb, debug=False):
3      if mb.board[1][1] == Marubatsu.EMPTY:
4          return [(1, 1)]
5      return ai6(mb, debug=debug, analyze=True)["candidate"]
```

### `ai_gt1` ~ `ai_gt6` の再定義

修正前の __`ai_gt1` ~ `ai_gt6`__ は、下記のプログラムのように __`mb` 以外の仮引数__ を持ちます。

```python
def ai_gt1(mb, mbtree):
略
```

__`@ai_by_candidate` のデコレーター式__ を利用する場合は、__2 つ目の仮引数に `debug` を記述__ する必要がありますが、下記のプログラムのように __仮引数 `debug` をデフォルト引数として定義__ すると、__デフォルト引数より後__ に __通常の仮引数 `mbtree` が定義__ されているため、__エラーが発生__ します。

```python
@ai_by_candidate
def ai_gt1(mb, debug=False, mbtree):
略
```

このような場合は、下記のプログラムの `mbtree=None` のように、__仮引数 `mbtree` に何らかのデフォルト値を設定__ する必要があります。`ai_gt1` の処理では `mbtree` に適切なデータが代入されている必要があるため、`mbtree` に対応する実引数を記述せずに `ai_gt1` を呼び出すと、`ai_gt1` の中の処理でエラーが発生します。

```python
@ai_by_candidate
def ai_gt1(mb, debug=False, mbtree=None):
略
```

:::note
下記のプログラムのように、`debug` をデフォルト引数としないようにするという方法もありますが、この場合は `ai_gt1` を呼び出す際に、__`debug` に対する実引数を省略できなくなる点が不便__ になります。

```python
@ai_by_candidate
def ai_gt1(mb, debug, mbtree):
略
```
:::

# 再定義した AI の関数の検証

再定義した AI の関数 __正しく動作するか__ を、それぞれの AI の関数を呼び出して検証するのは、AI の数が多いため大変です。本記事では他の検証方法として、__`gui_play` で AI を選択して AI どうしの対戦を行う__ ことで __検証する__ ことにします。ただし、__`gui_play`__ では、__`ai.py` の中で定義されている AI の関数__ を使って AI の対戦を行っています。__現状の ai.py には修正前の AI の関数が定義__ されているので、このままでは `gui_play` を利用して、再定義した AI の関数の検証を行うことはできません。

__今回の記事で再定義した AI の関数__ は ai_new.py に記述し、その内容は __次回の記事の ai.py に反映する__ ので、再定義した AI の __関数の検証は次回の記事で行う__ ことにします。

# 今回の記事の内容

今回の記事では、__評価値を利用__ して着手を選択する __AI の関数__ をデコレーター式 __`@ai_by_score` を利用して再定義__ しました。

また、__評価値を利用せず__ に着手を選択する __AI の関数__ に対する __デコレータの関数 `ay_by_candidate` を定義__ し、評価値を利用せずに着手を選択する AI の関数を __デコレーター式 `@ai_by_candidate` を利用して再定義__ しました。

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

| リンク | 説明 |
|:--|:--|
| [marubatsu.ipynb](https://github.com/ysgeso/marubatsu/blob/master/132/marubatsu.ipynb)  | 本記事で入力して実行した JupyterLab のファイル |
| [ai.py](https://github.com/ysgeso/marubatsu/blob/master/132/ai_new.py)  | 本記事で更新した ai_new.py  |

# 次回の記事

https://qiita.com/ysgeso/items/c05cb185873dda51c128

[^1]:`ai_by_score` が定義するラッパー関数では、候補手の一覧だけでなく、それぞれの合法手を着手した場合の局面の評価値の一覧も返しますが、評価値を計算しない AI の場合はそのデータを計算することは不可能なので、[以前の記事](https://qiita.com/ysgeso/items/2c76474ff139a824f4fd#%E8%A9%95%E4%BE%A1%E5%80%A4%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E5%87%A6%E7%90%86%E3%81%AE%E5%AE%9F%E8%A3%85)で説明したように、そのデータには `None` を設定します