Pythonで〇×ゲームのAIを一から作成する　その126　AI が計算した候補手または評価値のゲーム盤への表示

# 目次と前回の記事

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

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

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

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

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

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

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

# Marubatsu_GUI クラスの改良の続き

[以前の記事](https://qiita.com/ysgeso/items/2c4dcf4a73f08750811f#marubatsu_gui-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E6%94%B9%E8%89%AF)で、Marubatsu_GUI クラスの改良を開始しました。今回の記事ではその中の最後の、下記の改良の実装を開始します。

* ゲーム盤のマスに、そのマスに着手を行った際の AI の評価値を表示できるようにする

## AI が計算する評価値の表示のメリット

現状では、下記のプログラムのように、`ai14s` などの一部の AI の関数はキーワード引数 __`debug=True`__ を記述して AI の関数を呼び出すことで、それぞれの __合法手を着手した場合に計算される評価値を表示する__ ようになっています。

```python
from marubatsu import Marubatsu
from ai import ai14s

mb = Marubatsu()
ai14s(mb, debug=True)
```

実行結果

```
Start ai_by_score
Turn o
...
...
...

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

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

上記のようなデバッグの表示は、評価値を計算する際の __詳細なデータが表示されるという点では便利__ ですが、__一覧性に欠ける__ という欠点があります。そこで、ゲーム盤のそれぞれのマスに、そのマスに着手を行った局面に対して AI が計算する評価値を表示するという工夫を行うことにします。

また、`ai1` のように、__評価値を計算しない AI の場合__ はゲーム盤のマスに、その AI が計算した __候補手であることがわかるような、何らかの表示を行う__ ことにします。

# 評価値を表示するために必要な処理の実装

__現状の AI の関数__ は、以下の返り値を返します。

* 通常は、合法手の中から AI が選択した __1 つの着手を返す__
* キーワード引数 __`candidate=True`__ を記述して呼び出した場合は、その AI が最善手とみなした __候補手の一覧を list で返す__。ただし、現状では、一部の AI の関数のみこの機能が実装されている

合法手を着手した局面の評価値を表示するためには、上記以外に、それぞれの __合法手を着手した局面の評価値の情報__ を AI の関数が __返り値として返す必要__ があります。

そこで、AI の関数を以下のように修正することにします。

* 通常は、合法手の中から AI 選択した 1 つの着手を返す点は変わらない
* デフォルト値を `Falst` とする仮引数 __`analyze`__ を追加し、__`True` が代入された場合__ は、「その AI が最善手とみなした __候補手の一覧__」と「それぞれの __合法手を着手した局面の評価値__」を表すデータを下記の dict で返すようにする。このデータは、AI の処理を分析（analyze）するために利用できるので、キーワード引数の名前を `analyze` とした
    * __`candidate`__ というキーの値に、その AI が最善手とみなした __候補手の一覧を表す list__ を代入する
    * __`score_by_move`__ というキーの値に、それぞれの __合法手を着手した局面の評価値を表す dict__ を代入する。ただし、`ai1` のように、__評価値を計算しない__ AI の関数の場合は、__`None` を代入__ する
* __仮引数 `candidate`__ の機能は、仮引数 `analyze` の機能に含まれるので __削除する__

## `ai3s` の修正

まず、__評価値を計算する関数から修正__ することにします。そのような関数は `ai1s` から `ai14s` まで 14 種類もありますが、真ん中のマスに優先的に着手を行うという単純な [ルール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) を実装した __[`ai3s`](https://qiita.com/ysgeso/items/8f3d4b3d2c02ef51a70e#ai_by_score-%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E3%83%AB%E3%83%BC%E3%83%AB-3-%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) の修正を最初に行う__ ことにします。

具体的には `ai3s` は下記のプログラムのような単純な方法で評価値の計算を行います。

* 中央の (1, 1) に着手が行われていた場合は評価値として 1 を計算する
* それ以外のマスに着手を行がわれていた場合は評価値として 0 を計算する

```python
def ai3s(mb, debug=False, candidate=False):
    def eval_func(mb):
        if mb.last_move == (1, 1):
            return 1
        else:
            return 0
        
    return ai_by_score(mb, eval_func, debug=debug, candidate=candidate)
```

:::note
左上から順に空いているマスを探し、最初に見つかったマスに着手するという、[ルール 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) を実装した __[`ai1s`](https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf#ai1s-%E3%81%AE%E4%BF%AE%E6%AD%A3) を選択しなかった__ のは、キーワード引数 `rand=False` を記述して `ai_by_score` を呼び出すことで先頭の合法手を選択するという、__評価値をそのまま利用しない特殊な処理を行っているから__ です。そのため、`ai1s` の修正は、他の `ai2s` ~ `ai14s` とは若干異なる方法で行う必要があり、その修正は次回の記事で行います。

ランダムな着手を行うという、[ルール 2](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E3%81%AA%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) を実装した __[`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) を選択しなかった__ のは、常に全ての合法手が最善手となり、全ての空いているマスに `0` という評価値が表示されるため、__見た目があまり面白くない__ からです。

`ai_gt1` ~ `ai_gt6` も評価値を元に候補手を計算しますが、__その場で評価値を計算していない__ 点が `ai3s` と異なるため、修正方法は `ai3s` と大きく異なります。それらの修正は次回の記事で行います。
:::

まず、下記のプログラムのように `ai3s` を修正します。

* __1 行目__：仮引数 `candidate` を `analyze` に修正する
* __7 行目__：キーワード引数 `candidate=candidate` を `analyze=analyze` に修正する

```python
1  def ai3s(mb, debug=False, analyze=False):
2      if mb.last_move == (1, 1):
3          return 1
4      else:
5          return 0
6        
7      return ai_by_score(mb, eval_func, debug=debug, analyze=analyze)
```

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

```python
def ai3s(mb, debug=False, analyze=False):
    def eval_func(mb):
        if mb.last_move == (1, 1):
            return 1
        else:
            return 0
        
    return ai_by_score(mb, eval_func, debug=debug, analyze=analyze)
```
</details>

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

```diff_python
-def ai3s(mb, debug=False, candidate=False):
+def ai3s(mb, debug=False, analyze=False):
    def eval_func(mb):
        if mb.last_move == (1, 1):
            return 1
        else:
            return 0        
-   return ai_by_score(mb, eval_func, debug=debug, candidate=candidate)
+   return ai_by_score(mb, eval_func, debug=debug, analyze=analyze)
```
</details>

## `ai_by_score` の修正

次に、`ai3s` の 7 行目で呼び出す __`ai_by_score`__ を以下のプログラムのように修正します。

* __5 行目__：仮引数 `candidate` を `analyze` に修正する
* __7、8 行目__：`analyze` が `True` の場合に、「それぞれの合法手を着手した局面の評価値」を記録する `score_by_move` を空の dict で初期化する
* __12、13 行目__：`analyze` が `True` の場合に、`move` を着手した局面の評価値を `score_by_move` に記録する
* __14 ~ 18 行目__：`analyze` が `True` の場合に先程説明した dict を返り値として返すように修正する

```python
 1  from copy import deepcopy
 2  from random import choice
 3  from ai import dprint
 4
 5  def ai_by_score(mb_orig, eval_func, debug=False, rand=True, analyze=False):   
元と同じなので省略
 6      best_moves = []
 7      if analyze:
 8          score_by_move = {}
 9      for move in legal_moves:
元と同じなので省略
10          score = eval_func(mb)
11          dprint(debug, "score", score, "best score", best_score)
12          if analyze:
13              score_by_move[move] = score
元と同じなので省略
14      if analyze:
15          return {
16              "candidate": best_moves,
17              "score_by_move": score_by_move,
18          }
19      elif rand:   
20          return choice(best_moves)
21      else:
22          return best_moves[0]
```

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

```python
from copy import deepcopy
from random import choice
from ai import dprint

def ai_by_score(mb_orig, eval_func, debug=False, rand=True, analyze=False):   
    dprint(debug, "Start ai_by_score")
    dprint(debug, mb_orig)
    legal_moves = mb_orig.calc_legal_moves()
    dprint(debug, "legal_moves", legal_moves)
    best_score = float("-inf")
    best_moves = []
    if analyze:
        score_by_move = {}
    for move in legal_moves:
        dprint(debug, "=" * 20)
        dprint(debug, "move", move)
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        dprint(debug, mb)
        
        score = eval_func(mb)
        dprint(debug, "score", score, "best score", best_score)
        if analyze:
            score_by_move[move] = score
        
        if best_score < score:
            best_score = score
            best_moves = [move]
            dprint(debug, "UPDATE")
            dprint(debug, "  best score", best_score)
            dprint(debug, "  best moves", best_moves)
        elif best_score == score:
            best_moves.append(move)
            dprint(debug, "APPEND")
            dprint(debug, "  best moves", best_moves)

    dprint(debug, "=" * 20)
    dprint(debug, "Finished")
    dprint(debug, "best score", best_score)
    dprint(debug, "best moves", best_moves)
    if analyze:
        return {
            "candidate": best_moves,
            "score_by_move": score_by_move,
        }
    elif rand:   
        return choice(best_moves)
    else:
        return best_moves[0]
```
</details>

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

```diff_python
from copy import deepcopy
from random import choice
from ai import dprint

-def ai_by_score(mb_orig, eval_func, debug=False, rand=True, candidate=False):   
+def ai_by_score(mb_orig, eval_func, debug=False, rand=True, analyze=False):   
元と同じなので省略
    best_moves = []
+   if analyze:
+       score_by_move = {}
    for move in legal_moves:
元と同じなので省略
        score = eval_func(mb)
        dprint(debug, "score", score, "best score", best_score)
+       if analyze:
+           score_by_move[move] = score
元と同じなので省略
-   if candidate:
-       return best_moves
+   if analyze:
+       return {
+           "candidate": best_moves,
+           "score_by_move": score_by_move,
+       }
    elif rand:   
        return choice(best_moves)
    else:
        return best_moves[0]
```
</details>

上記の修正後に、下記のプログラムでキーワード引数 `analyze=True` を記述してゲーム開始時の局面に対して `ai3s` を呼び出すと、実行結果のように下記のような候補手の一覧と、それぞれの合法手を着手した局面の評価値を記録した dict が返されることが確認できます。なお、わかりやすさを重視して、結果を `pprint` で表示しました。

* 候補手の一覧は (1, 1) のみを要素とする list
* 局面の評価値は (1, 1) のキーの値が 1、それ以外のキーの値が 0 となる dict

```python
from pprint import pprint
 
pprint(ai3s(mb, analyze=True))
```

実行結果

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

また、下記のプログラムのように (1, 1) に着手を行った後で `ai3s` を実行すると、実行結果のように全ての合法手が候補手に、全ての着手に対する局面の評価値が 0 になることが確認できます。

```python
mb.move(1, 1)
pprint(ai3s(mb, analyze=True))
```

実行結果

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

## `ai3` の修正

次に、評価値を計算しない AI として、__`ai3s`__ と同じルール 3で候補手を計算する下記のプログラムで定義される `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)
```

### プログラムの改良

評価値を計算しない AI では、何らかの手順で候補手の一覧を計算し、その中からランダムに 1 つを選択するという処理を行う必要があります。

`ai3` では、(1, 1) に着手が行われていない場合は、(1, 1) を返し、そうでなければ合法手の一覧からランダムに 1 を選択して返すという処理を行っていますが、この 2 つの処理を下記の方法で 1 つにまとめることができます。

* (1, 1) のマスが開いている場合は、候補手の一覧として `(1, 1)` のみを要素として持つ `[(1, 1)]` という list を計算する
* (1, 1) のマスが開いていない場合は合法手の一覧を候補手の一覧とする
* 候補手の一覧の中からランダムに 1 つを選択して返す

上記の手順で正しい処理が行われる理由は、`[(1, 1)]` のような、要素が 1 つしかない list に対して `choice` でランダムに要素を選択した場合は、必ず `(1, 1)` が選択されるからです。下記は、そのように `ai3` を修正したプログラムです。

* __2、3 行目__：(1, 1) のマスが開いている場合に候補手を表す `candidate` に `(1, 1)` のみを要素とする list を代入するように修正する
* __4、5 行目__：(1, 1) のマスが開いていない場合は `candidate` に合法手の一覧を代入するように修正する。
* __6 行目__：`choice` を利用して候補手の中からランダムに一つを選択した着手を返す

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

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

```diff_python
def ai3(mb):
    if mb.board[1][1] == Marubatsu.EMPTY:
-       return (1, 1)]
+       candidate = [(1, 1)]
-   legal_moves = mb.calc_legal_moves()
-   return choice(legal_moves)
+   else:
+       candidate = mb.calc_legal_moves()
+   return choice(candidate)
```
</details>

:::note
3 行目を `candidate = (1, 1)` のように記述して `ai3` を呼び出してもエラーは発生しませんが、(1, 1) のマスが開いていた場合は __返り値に `1` が返る__ という __バグが発生する__ 点に注意して下さい。

その理由は、__`choice`__ が list や tuple などの __反復可能オブジェクトの中からランダムに 1 つの要素を選択する__ という処理を行うからです。`candidate` に `(1, 1)` が代入されていた場合は、__`choice((1, 1))`__ が実行され、__`(1, 1)` という tuple__ の中からランダムに 1 つの要素が選択されます。`(1, 1)` の要素はどちらも `1` なので、__必ず `1` が返される__ ことになります。
:::

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

次に、下記のプログラムのように `ai3` に仮引数 `analyze` を追加し、`analyze` が `True` の場合の処理を記述します。上記で __`ai3` を修正した理由__ は、下記のプログラムの 6 ~ 12 行目のように、__返り値を計算する処理__ を (1, 1) のマスが開いているかどうかに関わらず、__一つにまとめることができるようにするため__ です

* __1 行目__：仮引数 `analyze` を追加する
* __6 ~ 10 行目__：`analyze` が `True` の場合に返り値として返す dict を計算する。`ai3` は評価値を計算しないので、`score_by_move` のキーの値は `None` とする
* __11、12 行目__：`analyze` が `False` の場合は元と同じ処理を行う

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

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

```python
def ai3(mb, analyze=False):
    if mb.board[1][1] == Marubatsu.EMPTY:
        candidate = [(1, 1)]
    else:
        candidate = mb.calc_legal_moves()
    if analyze:
        return {
            "candidate": candidate,
            "score_by_move": None
        }
    else:
        return choice(candidate)
```
</details>

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

```diff_python
-def ai3(mb):
+def ai3(mb, analyze=False):
    if mb.board[1][1] == Marubatsu.EMPTY:
        candidate = [(1, 1)]
    else:
        candidate = mb.calc_legal_moves()
-   return choice(candidate)
+   if analyze:
+       return {
+           "candidate": candidate,
+           "score_by_move": None
+       }
+   else:
+       return choice(candidate)
```
</details>

上記の修正後に、下記のプログラムでキーワード引数 `analyze=True` を記述して `ai3` を実行すると、実行結果のように正しいデータが返されることが確認できます。

```python
mb.restart()
pprint(ai3(mb, analyze=True))
```

実行結果

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

また、下記のプログラムのように (1, 1) に着手を行った後で `ai3` を実行すると、実行結果のように全ての合法手が候補手になることが確認できます。

```python
mb.move(1, 1)
pprint(ai3(mb, analyze=True))
```

実行結果

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

## 残りの修正

この後で行う必要がある修正は以下の通りです。

* `ai_by_score` の仮引数 `candidate` を廃止したので、__キーワード引数 `candidate=candidate` を記述して AI の関数を呼び出す処理を修正__ する
* 残りの AI の関数を修正する

### キーワード引数 `candidate=candidate` を記述して AI の関数を呼び出す処理の修正

1 つ目の、キーワード引数 `candidate=candidate` を記述して AI の関数を呼び出す処理は、__util.py__ で定義されている __Check_solved__ クラスの __`is_strongly_solved`__ と __`is_weakly_solved_r`__ メソッドで記述されているので、それぞれを下記のプログラムのように修正します。

* __7 行目__：AI の関数呼び出しのキーワード引数 `candidate=True` を `analyze=True` に修正する。また、__返り値が dict になった__ ので、`candidate` にはその dict の __候補手の一覧を表す `candidate` のキーの値を代入__ するように修正する

```python
 1  from util import Check_solved, load_bestmoves, load_mblist
 2  from tqdm import tqdm
 3
 4  @staticmethod
 5  def is_strongly_solved(ai, params=None, consider_samedata=True):
元と同じなので省略
 6      for mb in tqdm(mblist):
 7          candidate = set(ai(mb, analyze=True, **params)["candidate"])
 8          bestmoves = set(Check_solved.bestmoves_by_board[mb.board_to_str()])
 9          if candidate <= bestmoves:
10              count += 1
11          else:
12              incorrectlist.append((mb, candidate, bestmoves))
元と同じなので省略
13
14  Check_solved.is_strongly_solved = is_strongly_solved
```

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

```python
from util import Check_solved, load_bestmoves, load_mblist
from tqdm import tqdm

@staticmethod
def is_strongly_solved(ai, params=None, consider_samedata=True):
    if Check_solved.bestmoves_by_board is None:
        Check_solved.bestmoves_by_board = load_bestmoves ("../data/bestmoves_by_board.dat")
    if consider_samedata:
        if Check_solved.mblist_by_board_min is None:
            Check_solved.mblist_by_board_min = load_mblist("../data/mblist_by_board_min.dat")
        mblist = Check_solved.mblist_by_board_min
    else:
        if Check_solved.mblist_by_board2 is None:
            Check_solved.mblist_by_board2 = load_mblist("../data/mblist_by_board2.dat") 
        mblist = Check_solved.mblist_by_board2        
    if params is None:
        params = {}
    count = 0
    incorrectlist = []
    for mb in tqdm(mblist):
        candidate = set(ai(mb, analyze=True, **params)["candidate"])
        bestmoves = set(Check_solved.bestmoves_by_board[mb.board_to_str()])
        if candidate <= bestmoves:
            count += 1
        else:
            incorrectlist.append((mb, candidate, bestmoves))
    nodenum = len(mblist)
    print(f"{count}/{nodenum} {count/nodenum*100:.2f}%")
    return count == nodenum, incorrectlist

Check_solved.is_strongly_solved = is_strongly_solved
```
</details>

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

```diff_python
from util import Check_solved, load_bestmoves, load_mblist
from tqdm import tqdm

@staticmethod
def is_strongly_solved(ai, params=None, consider_samedata=True):
元と同じなので省略
    for mb in tqdm(mblist):
-       candidate = set(ai(mb, candidate=True, **params))
+       candidate = set(ai(mb, analyze=True, **params)["candidate"])
        bestmoves = set(Check_solved.bestmoves_by_board[mb.board_to_str()])
        if candidate <= bestmoves:
            count += 1
        else:
            incorrectlist.append((mb, candidate, bestmoves))
元と同じなので省略

Check_solved.is_strongly_solved = is_strongly_solved
```
</details>

* __4 行目__：`is_strongly_solved` の 7 行目の修正と同じ

```python
1  @staticmethod
2  def is_weakly_solved_r(node, ai, turn, params, registered_boards):
元と同じなので省略
3      if turn == node.mb.turn:
4          moves = ai(node.mb, analyze=True, **params)["candidate"]
5      else:
6          moves = node.mb.calc_legal_moves()
元と同じなので省略
7
8  Check_solved.is_weakly_solved_r = is_weakly_solved_r
```

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

```python
@staticmethod
def is_weakly_solved_r(node, ai, turn, params, registered_boards):
    txt = node.mb.board_to_str()
    if txt in registered_boards:
        return True
    Check_solved.count += 1
    registered_boards.add(txt)
    if node.mb.status == turn or node.mb.status == Marubatsu.DRAW:
        return True
    elif node.mb.status != Marubatsu.PLAYING:
        return False
        
    if turn == node.mb.turn:
        moves = ai(node.mb, analyze=True, **params)["candidate"]
    else:
        moves = node.mb.calc_legal_moves()
    for move in moves:
        childnode = node.children_by_move[move]
        if not Check_solved.is_weakly_solved_r(childnode, ai, turn, params, registered_boards):
            return False
    return True 

Check_solved.is_weakly_solved_r = is_weakly_solved_r
```
</details>

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

```diff_python
@staticmethod
def is_weakly_solved_r(node, ai, turn, params, registered_boards):
元と同じなので省略
    if turn == node.mb.turn:
-       moves = ai(node.mb, candidate=True, **params)
+       moves = ai(node.mb, analyze=True, **params)["candidate"]
    else:
        moves = node.mb.calc_legal_moves()
元と同じなので省略

Check_solved.is_weakly_solved_r = is_weakly_solved_r
```
</details>

上記の修正後に、下記のプログラムを実行して __`is_strongly_solved`__ と __`is_weakly_solved`__ を使って __`ai3s` を判定__ すると、強解決でも、弱解決でもないという、正しい判定が行われることが確認できます。また、本記事での記述は省略しますが、`ai3` を判定しても同様の結果が得られるので、興味がある方は確認して下さい。

```python
Check_solved.is_strongly_solved(ai3s)
```

実行結果

```
100%|██████████| 431/431 [00:00<00:00, 3315.25it/s]
117/431 27.15%

(False,
 [(<marubatsu.Marubatsu at 0x286603756d0>, {(1, 2), (2, 1), (2, 2)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x286603759d0>, {(1, 2), (2, 2)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660375cd0>, {(2, 1), (2, 2)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660375fd0>, {(0, 2), (1, 2), (2, 1)}, {(0, 2)}),
  (<marubatsu.Marubatsu at 0x286603762d0>, {(0, 2), (1, 2)}, {(0, 2)}),
  (<marubatsu.Marubatsu at 0x286603765d0>, {(0, 2), (2, 1)}, {(0, 2)}),
  (<marubatsu.Marubatsu at 0x286603768d0>, {(1, 1)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660376bd0>, {(0, 2), (1, 2), (2, 2)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660376e90>, {(1, 2), (2, 2)}, {(1, 2)}),
  (<marubatsu.Marubatsu at 0x28660377190>, {(0, 2), (2, 2)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660377490>, {(1, 1)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660377790>, {(1, 1)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x286603792d0>, {(0, 2), (2, 1), (2, 2)}, {(2, 1)}),
  (<marubatsu.Marubatsu at 0x28660379590>, {(0, 2), (2, 1)}, {(2, 1)}),
  (<marubatsu.Marubatsu at 0x28660379b90>, {(1, 1)}, {(2, 2)}),
  (<marubatsu.Marubatsu at 0x28660379e90>, {(0, 2), (1, 2), (2, 1)}, {(2, 1)}),
  (<marubatsu.Marubatsu at 0x2866037a4d0>,
   {(0, 1), (0, 2), (1, 2), (2, 1), (2, 2)},
   {(1, 2)}),
略
```

```python
Check_solved.is_weakly_solved(ai3s)
```

実行結果

```
   o False
   x False
Both False
False
```

## 残りの AI の関数の修正について

AI の関数はこれまでに [20 種類以上作成した](https://qiita.com/ysgeso/items/10d1d01192c014173e4b) ので、上記の修正を行うのは大変です。そこで、残りの AI の関数の修正は次回の記事で行うことにして、__ゲーム盤のマスに__ そのマスに着手を行った局面に対する __`ai3s` の評価値を表示する処理__ を実装することにします。

なお、__`gui_play`__ で〇×ゲームを GUI で遊ぶ際に、__キーワード引数 `candidate` を記述して AI の関数を呼び出す処理を行っていない__ ので、下記のプログラムで __`gui_play` を実行しても問題なく動作します__。なお、実行結果はこれまでと同様なので省略します。

```python
from util import gui_play

gui_play()
```

# AI が計算した評価値または候補手の表示の実装

合法手を着手した局面の AI の評価値の表示を常に表示すると真剣勝負の邪魔になるので、[前回の記事](https://qiita.com/ysgeso/items/d8a9bd7e1ee8f7664ac5#marubatsu_gui-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE-create_widgets-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E4%BF%AE%E6%AD%A3)で作成した「状況」ボタンによってその表示を切り替えることにします。

また、具体的な表示は以下のようにすることにします。

* __評価値を計算する AI__ の場合は __マスの中__ に AI が計算した __評価値を表示__ する。また、そのマスが AI の __候補手に含まれている場合は赤字__ で、__含まれていない場合は黒字__ で表示する
* __評価値を計算しない AI の場合__ はマスの中に、そのマスが __AI の候補手に含まれている場合は「候補手」と表示__ し、そうでない場合は何も表示しない
* 上記の表示は[前回の記事](https://qiita.com/ysgeso/items/d8a9bd7e1ee8f7664ac5#%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%A3%E3%81%9F%E5%A0%B4%E5%90%88%E3%81%AE%E5%B1%80%E9%9D%A2%E3%81%AE%E7%8A%B6%E6%B3%81%E3%81%AE%E8%A1%A8%E7%A4%BA)で表示した「そのマスに着手を行った場合の局面の状況を表す文字」の下に表示する

また、上記の処理は、[前回の記事](https://qiita.com/ysgeso/items/d8a9bd7e1ee8f7664ac5#%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%A3%E3%81%9F%E5%A0%B4%E5%90%88%E3%81%AE%E5%B1%80%E9%9D%A2%E3%81%AE%E7%8A%B6%E6%B3%81%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%E5%87%A6%E7%90%86%E3%81%AE%E8%A8%98%E8%BF%B0%E5%A0%B4%E6%89%80)で Marubatsu_GUI クラスの __`update_gui`__ の中に記述した、「そのマスに着手を行った場合の局面の状況を表す文字を表示する処理」の後に記述することにします。

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

* __5 行目__：marubatsu.py を修正した際に循環インポートが発生しないようにここで `ai3s` をローカルにインポートしている
* __7 ~ 9 行目__：キーワード引数 `analyze=True` を記述して現在の局面に対して `ai3s` を呼び出し、その返り値から「それぞれの合法手を着手した場合に計算される評価値」と「候補手の一覧」を表すデータを取り出して `score_by_move` と `candidate` に代入する
* __18 ~ 20 行目__：`score_by_move` が `None` ではない場合は、19 行目で `move` が候補手に含まれているかどうかで文字の色を計算し、20 行目で `move` に着手した場合の評価値を表示する。評価値を表示する y 座標の値は試行錯誤して決めたものである
* __21、22 行目__：`score_by_move` が `None` であり、なおかつ `move` が候補手に含まれている場合は、「候補手」と表示する。なお、文字の大きさを `5*self.size` で計算して表示すると「候補手」の文字の右端がゲーム盤の枠に重なってしまうので小さくした

```python
 1  from marubatsu import Marubatsu_GUI
 2
 3  def update_gui(self):
元と同じなので省略
 4      if self.show_status:
 5          from ai import ai3s
 6          bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
 7          analyze = ai3s(self.mb, analyze=True)
 8          score_by_move = analyze["score_by_move"]
 9          candidate = analyze["candidate"]
10          for move in self.mb.calc_legal_moves():
11              x, y = move
12              mb = deepcopy(self.mb)
13              mb.move(x, y)
14              score = self.score_table[mb.board_to_str()]["score"]
15              color = "red" if move in bestmoves else "black"
16              text = calc_status_txt(score)
17              ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
18              if score_by_move is not None:
19                  color = "red" if move in candidate else "black"
20                  ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
21              elif move in candidate:
22                  ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
元と同じなので省略
23        
24  Marubatsu_GUI.update_gui = update_gui
```

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

```python
from marubatsu import Marubatsu_GUI

def update_gui(self):
    def calc_status_txt(score):
        if score > 0:
            return "〇"
        elif score == 0:
            return "△"
        else:
            return "×"
    
    ax = self.ax

    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()

    # y 軸を反転させる
    ax.invert_yaxis()

    # 枠と目盛りを表示しないようにする
    ax.axis("off")   

    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label}　VS　{self.dropdown_list[1].label}", 
            fontsize=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        score = self.score_table[self.mb.board_to_str()]["score"]
        if self.show_status:
            text += " " + calc_status_txt(score)
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(0, -0.2, text, fontsize=7*self.size)

    self.draw_board(ax, self.mb, lw=0.7*self.size)
    
    if self.show_status:
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
        analyze = ai3s(self.mb, analyze=True)
        score_by_move = analyze["score_by_move"]
        candidate = analyze["candidate"]
        for move in self.mb.calc_legal_moves():
            x, y = move
            mb = deepcopy(self.mb)
            mb.move(x, y)
            score = self.score_table[mb.board_to_str()]["score"]
            color = "red" if move in bestmoves else "black"
            text = calc_status_txt(score)
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
            if score_by_move is not None:
                color = "red" if move in candidate else "black"
                ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
            elif move in candidate:
                ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
                
    self.update_widgets_status()

    if hasattr(self, "mbtree_gui"):
        from tree import Node

        self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
        self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

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

```diff_python
from marubatsu import Marubatsu_GUI

def update_gui(self):
元と同じなので省略
    if self.show_status:
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
+       analyze = ai3s(self.mb, analyze=True)
+       score_by_move = analyze["score_by_move"]
+       candidate = analyze["candidate"]
-       for x, y in self.mb.calc_legal_moves():
+       for move in self.mb.calc_legal_moves():
+           x, y = move
            mb = deepcopy(self.mb)
            mb.move(x, y)
            score = self.score_table[mb.board_to_str()]["score"]
-           color = "red" if (x, y) in bestmoves else "black"
+           color = "red" if move in bestmoves else "black"
            text = calc_status_txt(score)
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
+           if score_by_move is not None:
+               color = "red" if move in candidate else "black"
+               ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
+           elif move in candidate:
+               ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

なお、元のプログラムでは __10 行目__ を `for x, y in self.mb.calc_legal_moves():` と記述していましたが、__着手を表すデータ__ を x 座標と y 座標にわけずに __そのまま記述する必要がある処理__ が 15、19、20、21 行目にあるため、上記の 10、11 行目のように修正しました。この修正を行うことで、例えば 15 行目で着手を記述する処理を元の `color = "red" if (x, y) in bestmoves else "black"` から、`color = "red" if move in bestmoves else "black"` のように記述することができます。

上記の修正後に下記のプログラムで `gui_play()` を実行して「分析」ボタンをクリックすると、実行結果の左図のように `ai3s` の候補手となる真ん中のマスに赤字の 1 が、それ以外のマスに黒字の 0 という __`ai3s` が計算した評価値が表示される__ ようになります。また、真ん中のマスに着手すると、全てのマスが `ai3s` の候補手になるので、実行結果の右図のように全てのマスに赤字の 0 が表示されます。

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/cb51f191-546a-9e5b-967d-ee72d996505d.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/329f4d2e-2175-e674-17c7-25c4876834c4.png" width="200">

また、下記のプログラムの 3、4 行目のように、__`ai3` で計算した評価値を表示__ するように `update_gui` を修正した後で、`gui_play()` を実行して「分析」ボタンをクリックすると、実行結果のように `ai3` が計算した候補手のマスに、__候補手という文字が表示される__ ことが確認できます。

```python
1  def update_gui(self):
元と同じなので省略
2      if self.show_status:
3          from ai import ai3
4          bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
5          analyze = ai3(self.mb, analyze=True)
6          score_by_move = analyze["score_by_move"]
7        candidate = analyze["candidate"]
元と同じなので省略
8        
9  Marubatsu_GUI.update_gui = update_gui
```

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

```python
def update_gui(self):
    def calc_status_txt(score):
        if score > 0:
            return "〇"
        elif score == 0:
            return "△"
        else:
            return "×"
    
    ax = self.ax

    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()

    # y 軸を反転させる
    ax.invert_yaxis()

    # 枠と目盛りを表示しないようにする
    ax.axis("off")   

    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label}　VS　{self.dropdown_list[1].label}", 
            fontsize=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        score = self.score_table[self.mb.board_to_str()]["score"]
        if self.show_status:
            text += " " + calc_status_txt(score)
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(0, -0.2, text, fontsize=7*self.size)

    self.draw_board(ax, self.mb, lw=0.7*self.size)
    
    if self.show_status:
        from ai import ai3
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
        analyze = ai3(self.mb, analyze=True)
        score_by_move = analyze["score_by_move"]
        candidate = analyze["candidate"]
        for move in self.mb.calc_legal_moves():
            x, y = move
            mb = deepcopy(self.mb)
            mb.move(x, y)
            score = self.score_table[mb.board_to_str()]["score"]
            color = "red" if move in bestmoves else "black"
            text = calc_status_txt(score)
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
            if score_by_move is not None:
                color = "red" if move in candidate else "black"
                ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
            elif move in candidate:
                ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
                
    self.update_widgets_status()

    if hasattr(self, "mbtree_gui"):
        from tree import Node

        self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
        self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

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

```diff_python
def update_gui(self):
元と同じなので省略
    if self.show_status:
-       from ai import ai3s
-       from ai import ai3
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
-       analyze = ai3s(self.mb, analyze=True)
+       analyze = ai3(self.mb, analyze=True)
        score_by_move = analyze["score_by_move"]
        candidate = analyze["candidate"]
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui
```
</details>

```python
gui_play()
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b9d7f7e5-7845-2173-17eb-8dae417a40dc.png" width="200">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/aac223e4-e4b6-2a25-9f33-8c764307695f.png" width="200">

# 今回の記事のまとめ

今回の記事では、`ai3` と `ai3s` が、候補手の一覧と、それぞれの合法手を着手した場合に計算される評価値を返すことができるように修正し、ゲーム盤のマスに、そのマスに着手を行った際の `ai3` の候補手や、`ai3s` の評価値を表示できるように修正しました。

現状では特定の AI の関数が計算した候補手または評価値しか表示できないので、次回の記事では他の AI の関数を修正し、マスに表示する評価値を計算する AI を切り替えることができるようにします。

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

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

# 次回の記事

https://qiita.com/ysgeso/items/1ce7ecc51ae323b64bee