Pythonで〇×ゲームのAIを一から作成する　その157　ミニマックス法の視覚化の検討と視覚化に必要なデータの記録

# 目次と前回の記事

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

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

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

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

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

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

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

# 置換表付き αβ 法の視覚化を行う方法の検討

前回の記事で、置換表付き αβ 法での評価値の計算手順を視覚化するためのデータを作成する __`calc_score_for_anim`__ を作成しましたので、次はそのデータを利用して __Mbtree_Anim で視覚化を行う処理を実装__ する必要があります。なお、Mbtree_Anim の下部に表示されるゲーム木の表示の部分は `calc_score_by_ab` と `calc_score_for_anim` のどちらで計算した場合でも表示する内容は同じなので、実装するのはその __上部に表示__ する α 値、β 値や計算したノードの数などの __アニメーションのフレームの情報を表示する部分__ です。

## 実装方法の選択

Mbtree_Anim は `calc_score_by_ab` で作成されたデータを利用して視覚化を行うように作られていますが、__`calc_score_for_anim` ではそれとは異なるデータを作成する__ ので、__以下のいずれかを行う必要__ があります。

* __Mbtree_Anim を修正__ して `calc_score_for_anim` で作成したデータに対する視覚化を行うことができるようにする
* __Mbtree_Anim とは別の__ `calc_score_for_anim` で作成したデータに対する視覚化を行う __クラスを定義__ する

__前者の方法__ の場合は、__互換性を重視して__ `calc_score_by_ab` と `calc_score_for_anim` の __両方で作られたデータに対応__ するか、__互換性を捨てて__ `calc_score_by_ab` で作られたデータを __利用できないようにするか__ を決める必要があります。

__互換性を重視__ する場合は、__過去に `calc_score_by_ab` で作成した__ `../data/abtree_root.mbtree` 等の __データを引き続き利用できる__ という __利点__ がありますが、両方のデータに対応できるようにするために __プログラムが複雑になる__ という __欠点__ があります。互換性を重視しない場合はその逆の性質を持ちます。

__後者の方法__ の場合は、`calc_score_by_ab` で作成したデータは引き続き Mbtree_Anim で利用でき、新しく作成するクラスは `calc_score_for_anim` で作成したデータだけに対応すればよいので __プログラムが複雑にならない__ という __利点__ がありますが、Mbtree_Anim と __大部分が似た新しいクラスを定義しなければならない__ という欠点があります[^1]。

どの方法にも一長一短がありますが、本記事では __互換性を重視__ して `calc_score_by_ab` と `calc_score_for_anim` の __両方で作られたデータに対応する__ ように __Mbtree_Anim を修正する__ という方法をとることにします。理由の一つとしては、実際にその方法で実装してみたところ、思っていたほとはプログラムが複雑にならなかったためです。ただ、人によっては感じ方が異なると思いますので、他の方法で実装したい人は自分がもっともふさわしいと思った方法で実装してみて下さい。

## `calc_score_for_anim` が行うことができる計算の種類

__`calc_score_for_anim`__ は置換表付き αβ 法だけでなく、下記のような __様々な条件で評価値を計算__ することができるので、__Mbtree_Anim__ がそれらの __全ての場合で適切な視覚化を行う__ 必要があります。なお、括弧の中に対応する仮引数を記述しました。

* アルゴリズムとして __ミニマックス法__ と __αβ 法__ のいずれかを選択できる（仮引数 `minimax`）
* __置換表を利用するか__ どうかを選択できる（仮引数 `use_tt`）
* __最短の勝利を目指す評価値を計算するか__ どうかを選択できる（仮引数 `shortest_victory`）
* __ルートノードの α 値と β 値の初期値__ を評価値の最小値と最大値に設定するかどうかを選択できる（仮引数 `init_ab`）

4 種類の True か False を選択する設定があるので合計 $2^4 = 16$ __通りの設定__ があります。

## 現状の Mbtree_Anim の問題点の考察

それぞれの場合の表示方法を検討するために、__現状の Mbtree_Anim の問題点を考察__ することにします。

__Mbtree_Anim__ は __もともと置換表を利用しない αβ 法__ での評価値を計算する手順を __視覚化するために作成__ したクラスです。それを __後から__ ミニマックス法の視覚化や、置換表を利用した場合などの __視覚化を行えるように修正__ したのですが __視覚化の方法__ が、下記のように __あまりわかりやすいとは言えない__ という問題があります。

* __どのような条件で評価値が計算されているか__ が表示されていないので __わかりづらい__
* 例えば __ミニマックス法__ で評価値を計算した場合でも __実際にはミニマックス法では利用しない α 値と β 値が常に表示される__ ため、表示している内容がミニマックス法で計算されたデータなのか、αβ 法で計算されたデータであるかの __区別がつけづらい__
* 置換表が利用されているかどうかなどの __他の条件__ も、見た目から __判別しづらい__

また、現時点で Mbtree_Anim が処理を行うことができる `calc_score_by_ab` で作成されたデータは __αβ 法で置換表を利用する場合__ は [置換表付き αβ 法](https://qiita.com/ysgeso/items/c43a1c7dc65b746c8394) ではなく、`(局面, α 値の初期値, β 値の初期値)` という __[3 つのデータをキーとする dict による置換表](https://qiita.com/ysgeso/items/dc738bc54c0f837f6dc5#3-%E3%81%A4%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E3%82%AD%E3%83%BC%E3%81%A8%E3%81%99%E3%82%8B%E7%BD%AE%E6%8F%9B%E8%A1%A8%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E4%BF%AE%E6%AD%A3%E6%96%B9%E6%B3%95)__ で処理を行っているので、以下のように置換表付き αβ 法での処理を __正しく視覚化することはできません__。

* 置換表付き αβ 法の視覚化で必要となる下記の表示が行われない
    * fail low、exact value、fail high の範囲の表示の色分けが行われない。なお、__現状の Mbtree_Anim__ が行う α 値と β 値による __範囲の色分け__ は α 値と β 値の初期値ではなく、子ノードの評価値によって __初期値から変化した可能性がある α 値と β 値による色分け__ なので、fail low、exact value、fail high の範囲で __色分けが行われない場合がある__
    * 置換表付き αβ 法が行う置換表に関する処理の表示が行なわれない
* `calc_score_by_ab` では、α 値または β 値をノードの評価値として計算しているが、__`calc_score_for_anim`__ では __ノードの評価値を α 値や β 値とは別に計算__ する。現状の Mbtree_Anim ではその表示に対応していない

## 実装の手順の検討

__問題点がある程度明確になった__ ので、それらの問題点を解決するような __表示方法を検討する__ ことにします。先ほど 16 通りの設定があると説明しましたが、__16 通りの設定に対する表示方法を一度に考えるのは大変__ なので、本記事ではミニマックス法と αβ 法の __それぞれの場合に分けて表示方法を検討する__ ことにします。

具体的には下記の手順で実装を行います。__今回の記事では下記の手順 1、2__ を行い、手順 3 以降は次回以降の記事で実装することにします。

1. __ミニマックス法__ での __視覚化の方法を検討__ する
2. 検討した __視覚化を行うために必要なフレームのデータを記録__ するように __`calc_score_for_anim` を修正__ する
3. __Mbtree_Anim を__ 検討した視覚化を行うように __修正する__
4. αβ 法で上記の 1 ~ 3 の手順を行う

# ミニマックス法の視覚化の検討

ミニマックス法での視覚化をどのように行えば良いかについて少し考えてみて下さい。

## ミニマックス法での視覚化の種類

先程 `calc_score_for_anim` は下記の 4 つの条件で評価値を計算できると説明しました。

* アルゴリズムとして __ミニマックス法__ と __αβ 法__ のいずれかを選択できる（仮引数 `minimax`）
* __置換表を利用するか__ どうかを選択できる（仮引数 `use_tt`）
* __最短の勝利を目指す評価値を計算するか__ どうかを選択できる（仮引数 `shortest_victory`）
* __ルートノードの α 値と β 値の初期値__ を評価値の最小値と最大値に設定するかどうかを選択できる（仮引数 `init_ab`）

ただし、__ミニマックス法では α 値と β 値を利用しない__ ので、ミニマックス法の場合で __設定できる条件__ は下記の __2 種類だけ__ です。従って、__組み合わせ__ は $2^2 = 4$ __通り__ です。

* __置換表を利用するか__ どうかを選択できる（仮引数 `use_tt`）
* __最短の勝利を目指す評価値を計算するか__ どうかを選択できる（仮引数 `shortest_victory`）

## 設定された条件の表示

[先程考察](#現状の-mbtree_anim-の問題点の考察)したように、現状の Mbtree_Anim では __どのような条件で評価値が計算されているかがわかりづらい__ という問題があるので、以下の方法でそれらを明確にすることにします。

* 「__ミニマックス法　置換表 〇　最短 ×　初期値 〇__」のように、「アルゴリズムの種類」、「置換表を利用しているかどうか」、「最短の勝利を目指す評価値を計算するかどうか」、「ルートノードの α 値と β 値を評価値の最小値と最大値で初期化するかどうか」について __設定された条件を表示__ する
* __ミニマックス法__ では α 値と β 値を利用しないので __「初期値 〇」の部分は表示しない__

さらに、下記の方法で __設定された条件が明確になる__ ような __工夫を行う__ ことにします。

### ミニマックス法と αβ 法での表示の区別

下記の方法で、__ミニマックス法と αβ 法のどちらで計算が行われたか__ を一目で区別できるようにするという工夫を行ないます。

* 現状の Mbtree_Anim では常に α 値と β 値を表示するが、__ミニマックス法__ ではそれらの値は利用しないので __α 値と β 値を表示しない__ ようにする
* 同様の理由で、__ミニマックス法__ では α 値と β 値による __評価値の範囲の色分けを行わない__ ようにする

### 最短の勝利を目指す評価値を計算しているかどうかの表示の区別

現状の Mbtree_Anim では、__常に__ 評価値の __数直線の範囲__ が正負の無限大と __-2 ~ 3 の範囲__ になっていますが、__最短の勝利を目指さない場合__ は数直線の範囲を正負の無限大と __-1 ~ 1 の範囲とする__ ことで一目で区別できるようにするという工夫を行ないます。

### 以後の説明に関する補足

上記の工夫について毎回記述するのは大変なので、以後の検討では __これらの工夫による表示が行われるという前提__ で行います。

## それぞれの状態での表示の検討

次に、ミニマックス法での "start"、"tt"、"score"、"update"、"end" の __5 つのそれぞれの状態のフレーム__ に対して、__どのような表示を行うか__ について検討する必要があります。どのように表示すれば良いかについて少し考えてみて下さい。

### 共通する表示内容の検討

最初に、すべての場合で __共通する表示内容__ について検討することにします。

評価値を表示する __数直線は必ず表示する必要__ があります。ただし、数直線の範囲は先ほど説明したように最短の勝利を目指す評価値を計算するかによって変化する可能性があります。

現状の Mbtree_Anim では、真ん中の上の行に「深さ 0 max node 処理の開始」のように、「選択中のノードの深さ」、「ノードの種類」、「フレームで行われる処理を表す状態」を表す内容が表示されます。これは __すべての場合で表示したほうが良い情報__ なので、__常に表示する__ ことにします。なお、他の表示内容とのバランスを考えて、表示位置を現状の Mbtree_Anim から変更する可能性はあります。

右に表示される「計算済」や「枝狩り」などの、__枝狩りが行われたノードの数などの表示__ も常に表示したほうが良い情報です。また、`calc_score_by_ab` と `calc_score_for_anim` で表示する内容を __変える必要は特にない__ ので、現状の __Mbtree_Anim と同じ内容を表示__ することにします。

以後は、上記の情報を常に表示するという前提で説明を行います。

### 条件による表示の違いの検討

先程説明したように、ミニマックス法では下記の 2 種類の条件で評価値を計算することができます。

* __置換表を利用するか__ どうかを選択できる（仮引数 `use_tt`）
* __最短の勝利を目指す評価値を計算するか__ どうかを選択できる（仮引数 `shortest_victory`）

5 つの状態の中で、__置換表を利用するかどうか__ によって __行う処理が変わる__ のは __`"tt"`__ と __`"end"`__ の状態のフレームだけで、以下のように異なります。従って、__`"end"` のフレームの表示のみ__、置換表を利用するかどうかで __表示が変わる場合があります__。

* __`"tt"`__ のフレームの処理は __置換表を利用する場合のみ__ 行なわれる
* __`"end"`__ のフレームの処理では、__置換表を利用する場合のみ__ 計算したノードの評価値を __置換表に登録する処理が行われる__ 場合がある

__最短の勝利を目指す評価値を計算するかどうか__ によって変わるのは、__決着がついたノードの評価値の計算方法だけ__ で、それ以外の処理は全く変わりません。そのため、5 つの状態の表示方法は、先程説明した __数直線の表示範囲を変えるという工夫以外__ では __共通します__。

上記の考察を元に、5 つのそれぞれの状態での表示方法を検討することにします。

### "start" の状態の表示

__ノードの処理の開始__ を表す __"start"__ の状態のフレームでは、常に表示する内容を除いて __特に何かを表示する必要はない__ でしょう。

### "tt" の状態の表示

__置換表に関する処理が行われた__ ことを表す __"tt"__ の状態のフレームでは、ノードの評価値が __置換表に登録されている場合__ と、__そうでない場合__ の 2 種類の処理が行われます。そこで、以下の表示を行うことにします。

* ノードの評価値が置換表に __登録されている__ 場合は __赤字で「置換表に登録済」__ と表示し、__数直線上__ に登録されていた __評価値を表示__ する
* __登録されていない__ 場合は __黒字で「置換表に未登録」__ と表示する

### "score" の状態の表示

__子ノードの評価値が計算された__ ことを表す __"score"__ の状態のフレームでは、__数直線上に以下の表示__ を行うことにします。

* 計算された __子ノードの評価値__
* 子ノードの評価値で更新する前の、__その時点でのノードの評価値__

### "update" の状態の表示

子ノードの評価値を使ってこのノードの __評価値を更新する処理__ を行う __"update"__ の状態のフレームでは、以下の表示を行うことにします。

* ノードの __評価値が更新__ された場合は __赤字で「評価値の更新」__ と表示する
* __更新されていない__ 場合は __黒字で「評価値の更新なし」__ と表示する
* いずれの場合でも、__数直線上__ にノードの __評価値を表示する__

### "end" の状態の表示

ノードの __評価値が確定__ したことを表す __"end"__ の状態のフレームでは、以下の表示を行うことにします。

* __数直線上__ に確定した __ノードの評価値を表示__ する
* 置換表を __利用する場合__ は、以下の表示を行う
    * このノードの評価値が置換表に __登録されていなかった__ 場合は置換表への登録が行われるので __赤字で「置換表への登録」__ と表示する
    * __登録されていた__ 場合は __黒字で「置換表に登録されていたデータを利用」__ と表示する
* 置換表を __利用しない場合__ は __何も表示しない__

# 表示内容と表示に必要な情報の検討

表示する内容が検討できたので、上記で検討した __表示内容を整理__ し、その内容を __表示するために必要な情報を検討__ します。

## 常に表示する内容

常に表示する内容と、表示するために必要な情報は以下の表の通りです。なお、`mbtree` は Mbtree_Anim で表示を行うゲーム木のデータ、`node` は表示するフレームで処理を行うノードのデータ表すものとします。

|表示内容|必要な情報|
|:--|:--|
|__設定された条件の表示__|`mbtree.minimax`<br>`mbtree.use_tt`<br>`mbtree.shortest_victory`<br>`mbtree.init_ab`|
|__数直線__|`mbtree.shortest_victory`[^2]|
|__深さ__|`node.mb.move_count`|
|__ノードの種類__|`node.mb.move_count`|
|__フレームの状態__|フレームの状態を表す文字列|
|__枝狩りが行われた数などの情報__|計算を行ったノードの数と枝狩りを行ったノードの数|

ただし、数直線の表示範囲は負の無限大と正の無限大の間に、以下の __評価値の最小値と最大値の範囲__ を表示することにします。

* 最短の評価値を目指さない場合は -1 ~ 1 の範囲
* 最短の評価値を目指す場合は -2 ~ 3 の範囲

__`mbtree`__ と __`node`__ の情報は Mbtree_Anim で処理を行う際に __利用でき__ るので、常に表示する内容に対して __必要となるデータ__ は以下の通りです。

* フレームの状態を表す文字列
* 計算が行われたノードの数
* 枝狩りが行われたノードの数

また、これらの情報は下記のプログラムのように、__`calc_score_by_ab`__ でもフレームの情報を記録する際の tuple の 3 ~ 5 番の要素に __記録__ しています。下記の `"start"` がフレームの状態、`self.num_calculated` と `self.num_pruned` が計算が行われたノードの数と、枝狩りが行われたノードの数を表します。

```python
    self.ablist_by_score.append((alpha, beta, None, "start",
                                 self.num_calculated, self.num_pruned))
```

## フレームの状態ごとに表示する内容

それぞれのフレームの状態で表示する内容は以下の通りです。

* __"start"__ の状態
なし
* __"tt"__ の状態
    * 置換表にノードの評価値が登録されている場合
        * 赤字で「置換表に登録済」を表示
        * 数直線上に置換表に登録されていた評価値を表示
    * 登録されていない場合
        * 黒字で「置換表に未登録」を表示
* __"score"__ の状態
    * 数直線上にそのフレームでのノードの評価値を表示
    * 数直線上に子ノードの評価値を表示
* __"update"__ の状態
    * 数直線上にそのフレームでのノードの評価値を表示
    * ノードの評価値が更新された場合
        * 赤字で「評価値の更新」を表示
    * 更新されていない場合  
        * 黒字で「評価値の更新なし」を表示
* __"end"__ の状態
    * 数直線上にそのフレームでのノードの評価値を表示
    * 置換表を利用する場合
        * 置換表にノードの評価値が登録されている場合
            * 黒字で「置換表に登録されていたデータを利用」を表示
        * 登録されていない場合
            * 赤字で「置換表への登録」を表示


また、それぞれの状態で必要となる情報は以下の通りです。

* __"start"__ の状態
なし
* __"tt"__ の状態
    * 置換表にノードの評価値が登録されているかどうか
    * 登録されている場合は、置換表に登録されていた評価値
* __"score"__ の状態
    * そのフレームでのノードの評価値
    * 子ノードの評価値
* __"update"__ の状態
    * そのフレームでのノードの評価値
    * ノードの評価値が更新されたかどうか
* __"end"__ の状態
    * そのフレームでのノードの評価値
    * 置換表を利用する場合は「置換表にノードの評価値が登録されているかどうか」

それぞれの状態で __必要となるデータを表にまとめる__ と以下のようになります。データが必要な場合は 〇 を表記しました。

|データ|"start"|"tt"|"score"|"update"|"end"|
|:--|:-:|:-:|:-:|:-:|:-:|
|置換表に登録されているかどうか|×|〇|×|×|〇|
|置換表に登録されていた評価値|×|〇|×|×|×|×|
|そのフレームでのノードの評価値|×|×|〇|〇|〇|
|子ノードの評価値|×|×|〇|×|×|
|ノードの評価値が更新されたかどうか|×|×|×|〇|×|

# `calc_score_for_anim` の修正

[前回の記事](https://qiita.com/ysgeso/items/bcd2526d5327fd1e72e9#%E7%BD%AE%E6%8F%9B%E8%A1%A8%E4%BB%98%E3%81%8D-%CE%B1%CE%B2-%E6%B3%95%E3%81%AE%E8%A6%96%E8%A6%9A%E5%8C%96%E3%82%92%E8%A1%8C%E3%81%86%E3%81%9F%E3%82%81%E3%81%AE%E9%96%A2%E6%95%B0%E3%81%AE%E5%AE%9A%E7%BE%A9)では、__`calc_score_for_anim`__ の中で __フレームの情報を記録__ する部分を __修正しませんでした__ が、その理由は __どのようなデータを記録すればよいか__ が、表示する内容が決まらないと __わからなかったから__ です。表示に必要なデータが整理できたので、__上記のデータを記録する__ ように __`calc_score_for_anim` を修正__ することにします。なお、`calc_score_for_anim` の修正にあたっては、以下のような工夫を行なうことにしました。

* 今回の記事の最初で __互換性を重視__ して __Mbtree_Anim__ が `calc_score_by_ab` と `calc_score_for_anim` の __両方で作成されたデータを表示できるようにする__ ことにした。そのため、__それぞれの場合__ で __表示する処理を変える必要__ がある。そこで、Mbtree に __どちらのメソッドで作成された__ データであるかを __区別できるようにする__ ための、__`calculated_by_calc_score_for_anim`__ という __属性を追加__ した
* __`calc_score_by_ab`__ では、フレームの情報を記録する __`ablist_by_score`__ という list の要素に __tuple でフレームの情報を記録していた__ が、tuple ではそれぞれの要素に代入された値がどのような意味を持つかが __わかりづらい__ ので、__dict を使って__ 下記の表のような __データを記録する__ ようにした。また、dict でデータを記録することで、tuple のように記録する必要がないデータの要素に `None` を代入する必要がなくなるという利点が得られる
* __`calc_score_for_anim`__ では __置換表に記録するデータ__ は、__ミニマックス値の下界と上界__ であり、__ミニマックス法__ の場合は __置換表の下界と上界の両方にミニマックス値が記録される__。ミニマックス法の場合はフレームの情報にミニマックス値を表すデータを 1 つだけ記録してもかまわないが、今後の記事で __置換表付き αβ 法の視覚化__ を行う際に __下界と上界の両方の値が必要となる__ ので、下記の表のように置換表に登録されていたミニマックス値の下界と上界の __両方を "tt" のフレームのデータに記録__ することにした

|キー|キーの値の意味|
|:--|:--|
|__"status"__|フレームの状態を表す文字列|
|__"num_calculated"__|計算されたノードの数|
|__"num_pruned"__|枝狩りされたノードの数|
|__"registered_in_tt"__|置換表にノードの評価値が登録されていたかどうか|
|__"lower_bound"__|ミニマックス値の下界|
|__"upper_bound"__|ミニマックス値の上界|
|__"score"__|そのフレームでのノードの評価値|
|__"childscore"__|子ノードの評価値|
|__"updated"__|ノードの評価値が更新されたかどうか|

なお、`calc_score_for_anim` では、置換表に __登録されていない場合__ はミニマックス値の __下界と上界として評価値の最小値と最大値が設定__ されますが、その場合はそれらの値は __`"tt"` のフレームの表示では利用しない__ ので問題はありません。

下記は、そのように修正した `calc_score_for_anim` のプログラムです。なお、__前回の記事で定義__ した __`calc_score_for_anim`__ には __フレームの記録の処理に関するバグがありました__ ので、その修正も行なっています。ただし、そのバグは [前回の記事](https://qiita.com/ysgeso/items/bcd2526d5327fd1e72e9#calc_score_of_anim-%E3%81%AE%E5%AE%9F%E8%A3%85%E3%81%A8%E7%A2%BA%E8%AA%8D) で表示した __枝狩りが行われた数などの計算には影響を与えない__ ので、前回の記事のプログラムは修正しないことにします。なお、下記のプログラムは非常に長いですが、修正した内容のほとんどはフレームの情報を記録する処理なので、見た目ほどの大きな変更ではありません。

* __7 ~ 11、25 ~ 32、42 ~ 48、66 ~ 72、78 ~ 84 行目__：`ablist_by_score` に記録するフレームの情報を、tuple から dict に修正し、それぞれのフレームの状態で必要となる情報を記録するように修正した
* __13 行目__：置換表に登録されていたかどうかを表す変数を `False` で初期化する
* __20 行目__：置換表に登録されていた場合に上記の変数を `True` にする
* __50、56 行目__：`"update"` のフレームでノードの評価値が更新されるかどうかを計算して `updated` に代入する。`"updated"` は 69 行目でこのフレームの情報として記録する  
* __88 行目__：`calc_score_for_anim` によって計算されたことがわかるように、`calculated_by_calc_score_for_anim` 属性に `True` を代入するようにした

バグの修正は以下の通りです。

* __21、61 行目__：枝狩りを行う場合にノードを `nodelist_by_score` に登録する処理を、枝狩りが行なわれた数を数える処理の後で行なうように記述していたが[^3]、そのようにすると枝狩りが一つ前のフレームで行なわれてしまうようになるので、枝狩りが行われた数を数える処理の前に移動した
* __25 ~ 32 行目__：状態が `"tt"` の場合に `ablist_by_score` にフレームの情報を記録する処理のインデントが間違っていたため、`"use_tt"` が `False` の場合も `"tt"` の状態のフレームが記録されるようになっている点を修正した

```python
 1  from marubatsu import Marubatsu
 2  from tree import Mbtree
 3  
 4  def calc_score_for_anim(self, abroot, debug=False, minimax=False, init_ab=False, use_tt=False, shortest_victory=None):
元と同じなので省略
 5      def calc_node_score(node, tt, alphaorig=float("-inf"), betaorig=float("inf")):
元と同じなので省略
 6          self.nodelist_by_score.append(node)
 7          self.ablist_by_score.append({
 8              "status": "start",
 9              "num_calculated": self.num_calculated,
10              "num_pruned": self.num_pruned,
11          })
12          
13          registered_in_tt = False
14          if node.mb.status != Marubatsu.PLAYING:
元と同じなので省略
15          else:
16              pruned = False
17              if use_tt:
18                  boardtxt = node.mb.board_to_str()
19                  if boardtxt in tt:
20                      registered_in_tt = True
元と同じなので省略
21                  self.nodelist_by_score.append(node)
22                  if pruned:
23                      for childnode in node.children:
24                          assign_pruned_index(childnode, len(self.nodelist_by_score) - 1)  
25                  self.ablist_by_score.append({
26                      "status": "tt",
27                      "registered_in_tt": registered_in_tt,
28                      "lower_bound": lower_bound,
29                      "upper_bound": upper_bound,
30                      "num_calculated": self.num_calculated,
31                      "num_pruned": self.num_pruned,
32                  })   
33          
34              if not pruned:
35                  alpha = alphaorig
36                  beta = betaorig
37                  maxnode = node.mb.turn == Marubatsu.CIRCLE
38                  node.score = min_score if maxnode else max_score
39                  for childnode in node.children:
40                      childscore = calc_node_score(childnode, tt, alpha, beta)
41                      self.nodelist_by_score.append(node)
42                      self.ablist_by_score.append({
43                          "status": "score",
44                          "score": node.score,
45                          "childscore": childscore,
46                          "num_calculated": self.num_calculated,
47                          "num_pruned": self.num_pruned,
48                      })   
49                      if maxnode:
50                          updated = node.score < childscore
51                          node.score = max(node.score, childscore)
52                          alpha = max(node.score, alpha)
53                          if node.score >= beta:
54                              pruned = True
55                      else:
56                          updated = node.score > childscore
57                          node.score = min(node.score, childscore)
58                          beta = min(node.score, beta)
59                          if node.score <= alpha:
60                              pruned = True
61                      self.nodelist_by_score.append(node)
62                      if pruned:
63                          index = node.children.index(childnode)
64                          for prunednode in node.children[index + 1:]:
65                              assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
66                      self.ablist_by_score.append({
67                          "status": "update",
68                          "score": node.score,
69                          "updated": updated,
70                          "num_calculated": self.num_calculated,
71                          "num_pruned": self.num_pruned,
72                      })   
73                      if pruned:
74                          break 
75  
元と同じなので省略
76          self.nodelist_by_score.append(node)
77          self.num_calculated += 1     
78          self.ablist_by_score.append({
79              "status": "end",
80              "score": node.score,
81              "registered_in_tt": registered_in_tt,
82              "num_calculated": self.num_calculated,
83              "num_pruned": self.num_pruned,
84          })   
85          node.score_index = len(self.nodelist_by_score) - 1          
86          return node.score                
87                                      
88      self.calculated_by_calc_score_for_anim = True
89      self.minimax = minimax
元と同じなので省略
90      
91  Mbtree.calc_score_for_anim = calc_score_for_anim
```

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

```python
from marubatsu import Marubatsu
from tree import Mbtree

def calc_score_for_anim(self, abroot, debug=False, minimax=False, init_ab=False, use_tt=False, shortest_victory=None):
    def assign_pruned_index(node, index):
        node.pruned_index = index
        self.num_pruned += 1
        for childnode in node.children:
            assign_pruned_index(childnode, index)
            
    def calc_node_score(node, tt, alphaorig=float("-inf"), betaorig=float("inf")):
        if minimax:
            alphaorig = float("-inf")
            betaorig = float("inf")            

        self.nodelist_by_score.append(node)
        self.ablist_by_score.append({
            "status": "start",
            "num_calculated": self.num_calculated,
            "num_pruned": self.num_pruned,
        })
        
        registered_in_tt = False
        if node.mb.status != Marubatsu.PLAYING:
            self.calc_score_of_node(node)
            lower_bound = node.score
            upper_bound = node.score
        else:
            pruned = False
            if use_tt:
                boardtxt = node.mb.board_to_str()
                if boardtxt in tt:
                    registered_in_tt = True
                    lower_bound, upper_bound = tt[boardtxt]
                    if lower_bound == upper_bound:
                        node.score = lower_bound
                        pruned = True
                    elif upper_bound <= alphaorig:
                        node.score = upper_bound
                        pruned = True
                    elif betaorig <= lower_bound:
                        node.score = lower_bound
                        pruned = True
                    else:
                        alphaorig = min(alphaorig, lower_bound)
                        betaorig = max(betaorig, upper_bound)
                else:
                    lower_bound = min_score
                    upper_bound = max_score
                self.nodelist_by_score.append(node)
                if pruned:
                    for childnode in node.children:
                        assign_pruned_index(childnode, len(self.nodelist_by_score) - 1)  
                self.ablist_by_score.append({
                    "status": "tt",
                    "registered_in_tt": registered_in_tt,
                    "lower_bound": lower_bound,
                    "upper_bound": upper_bound,
                    "num_calculated": self.num_calculated,
                    "num_pruned": self.num_pruned,
                })   
        
            if not pruned:
                alpha = alphaorig
                beta = betaorig
                maxnode = node.mb.turn == Marubatsu.CIRCLE
                node.score = min_score if maxnode else max_score
                for childnode in node.children:
                    childscore = calc_node_score(childnode, tt, alpha, beta)
                    self.nodelist_by_score.append(node)
                    self.ablist_by_score.append({
                        "status": "score",
                        "score": node.score,
                        "childscore": childscore,
                        "num_calculated": self.num_calculated,
                        "num_pruned": self.num_pruned,
                    })   
                    if maxnode:
                        updated = node.score < childscore
                        node.score = max(node.score, childscore)
                        alpha = max(node.score, alpha)
                        if node.score >= beta:
                            pruned = True
                    else:
                        updated = node.score > childscore
                        node.score = min(node.score, childscore)
                        beta = min(node.score, beta)
                        if node.score <= alpha:
                            pruned = True
                    self.nodelist_by_score.append(node)
                    if pruned:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
                    self.ablist_by_score.append({
                        "status": "update",
                        "score": node.score,
                        "updated": updated,
                        "num_calculated": self.num_calculated,
                        "num_pruned": self.num_pruned,
                    })   
                    if pruned:
                        break 
                    
        if use_tt:
            from util import calc_same_boardtexts
            boardtxtlist = calc_same_boardtexts(node.mb)
            
            if node.score <= alphaorig:
                upper_bound = node.score
            elif node.score < betaorig:
                lower_bound = node.score
                upper_bound = node.score
            else:
                lower_bound = node.score
            for boardtxt in boardtxtlist:
                tt[boardtxt] = (lower_bound, upper_bound) 

        self.nodelist_by_score.append(node)
        self.num_calculated += 1     
        self.ablist_by_score.append({
            "status": "end",
            "score": node.score,
            "registered_in_tt": registered_in_tt,
            "num_calculated": self.num_calculated,
            "num_pruned": self.num_pruned,
        })   
        node.score_index = len(self.nodelist_by_score) - 1          
        return node.score                
                                    
    self.calculated_by_calc_score_for_anim = True
    self.minimax = minimax
    self.init_ab = init_ab
    self.use_tt = use_tt
    if shortest_victory is not None:
        self.shortest_victory = shortest_victory

    from ai import dprint       
    for node in self.nodelist:
        node.score_index = float("inf")
        node.pruned_index = float("inf")
    self.nodelist_by_score = []
    self.ablist_by_score = []
    self.num_calculated = 0
    self.num_pruned = 0
    if init_ab:
        min_score = -2 if self.shortest_victory else -1
        max_score = 3 if self.shortest_victory else 1
    else:
        min_score = float("-inf")
        max_score = float("inf")
    calc_node_score(abroot, {}, min_score, max_score)
    total_nodenum = self.num_pruned + self.num_calculated
    ratio = self.num_calculated / total_nodenum * 100
    dprint(debug, "ミニマックス法で計算したか", minimax)    
    dprint(debug, "計算したノードの数",  self.num_calculated)
    dprint(debug, "枝狩りしたノードの数",  self.num_pruned)
    dprint(debug, "合計",  total_nodenum)
    dprint(debug, f"割合 {ratio:.1f}%") 
    
Mbtree.calc_score_for_anim = calc_score_for_anim
```
</details>

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

```diff_python
from marubatsu import Marubatsu
from tree import Mbtree

def calc_score_for_anim(self, abroot, debug=False, minimax=False, init_ab=False, use_tt=False, shortest_victory=None):
元と同じなので省略
    def calc_node_score(node, tt, alphaorig=float("-inf"), betaorig=float("inf")):
元と同じなので省略
        self.nodelist_by_score.append(node)
-       self.ablist_by_score.append((alphaorig, betaorig, None, "start",
-                                    self.num_calculated, self.num_pruned))
+       self.ablist_by_score.append({
+           "status": "start",
+           "num_calculated": self.num_calculated,
+           "num_pruned": self.num_pruned,
+       })
        
+       registered_in_tt = False
        if node.mb.status != Marubatsu.PLAYING:
元と同じなので省略
        else:
            pruned = False
            if use_tt:
                boardtxt = node.mb.board_to_str()
                if boardtxt in tt:
+                   registered_in_tt = True
元と同じなので省略
+               self.nodelist_by_score.append(node)
                if pruned:
                    for childnode in node.children:
                        assign_pruned_index(childnode, len(self.nodelist_by_score) - 1)  
-           self.nodelist_by_score.append(node)
-           self.ablist_by_score.append((alphaorig, betaorig, None, "tt",
-                                        self.num_calculated, self.num_pruned))
+               self.ablist_by_score.append({
+                   "status": "tt",
+                   "registered_in_tt": registered_in_tt,
+                   "lower_bound": lower_bound,
+                   "upper_bound": upper_bound,
+                   "num_calculated": self.num_calculated,
+                   "num_pruned": self.num_pruned,
+               })   
        
            if not pruned:
                alpha = alphaorig
                beta = betaorig
                maxnode = node.mb.turn == Marubatsu.CIRCLE
                node.score = min_score if maxnode else max_score
                for childnode in node.children:
                    childscore = calc_node_score(childnode, tt, alpha, beta)
                    self.nodelist_by_score.append(node)
-                   self.ablist_by_score.append((alpha, beta, node.score, "score",
-                                                self.num_calculated, self.num_pruned))
+                   self.ablist_by_score.append({
+                       "status": "score",
+                       "score": node.score,
+                       "childscore": childscore,
+                       "num_calculated": self.num_calculated,
+                       "num_pruned": self.num_pruned,
+                   })   
                    if maxnode:
+                       updated = node.score < childscore
                        node.score = max(node.score, childscore)
                        alpha = max(node.score, alpha)
                        if node.score >= beta:
                            pruned = True
                    else:
+                       updated = node.score > childscore
                        node.score = min(node.score, childscore)
                        beta = min(node.score, beta)
                        if node.score <= alpha:
                            pruned = True
+                   self.nodelist_by_score.append(node)
                    if pruned:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
-                   self.nodelist_by_score.append(node)
-                   self.ablist_by_score.append((alpha, beta, None, "update",
-                                                self.num_calculated, self.num_pruned))
+                   self.ablist_by_score.append({
+                       "status": "update",
+                       "score": node.score,
+                       "updated": updated,
+                       "num_calculated": self.num_calculated,
+                       "num_pruned": self.num_pruned,
+                   })   
                    if pruned:
                        break 
                    
元と同じなので省略
        self.nodelist_by_score.append(node)
        self.num_calculated += 1     
-       self.ablist_by_score.append((alphaorig, betaorig, None, "end",
-                                    self.num_calculated, self.num_pruned))        
+       self.ablist_by_score.append({
+           "status": "end",
+           "score": node.score,
+           "registered_in_tt": registered_in_tt,
+           "num_calculated": self.num_calculated,
+           "num_pruned": self.num_pruned,
+       })   
        node.score_index = len(self.nodelist_by_score) - 1          
        return node.score                
                                    
+   self.calculated_by_calc_score_for_anim = True
    self.minimax = minimax
元と同じなので省略
    
Mbtree.calc_score_for_anim = calc_score_for_anim
```
</details>

上記の修正後に下記のプログラムで __[前回の記事](https://qiita.com/ysgeso/items/bcd2526d5327fd1e72e9#calc_score_of_anim-%E3%81%AE%E5%AE%9F%E8%A3%85%E3%81%A8%E7%A2%BA%E8%AA%8D)と同じ計算__ を行うと、実行結果から前回の記事と同じ結果が表示されることが確認できます。

```python
mbtree = Mbtree.load("../data/abtree_root")

print("ai_mmdfs")
mbtree.calc_score_for_anim(mbtree.root, minimax=True, init_ab=False, use_tt=False, debug=True)
print()
print("ai_mmdfs_tt")
mbtree.calc_score_for_anim(mbtree.root, minimax=True, init_ab=False, use_tt=True, debug=True)
print()
print("ai_abs_tt")
mbtree.calc_score_for_anim(mbtree.root, minimax=False, init_ab=False, use_tt=False, debug=True)
print()
print("ai_abs_tt3")
mbtree.calc_score_for_anim(mbtree.root, minimax=False, init_ab=False, use_tt=True, debug=True)
print()
print("ai_abs_tt4")
mbtree.calc_score_for_anim(mbtree.root, minimax=False, init_ab=True, use_tt=True, debug=True)
```

実行結果

```
ai_mmdfs
ミニマックス法で計算したか True
計算したノードの数 549946
枝狩りしたノードの数 0
合計 549946
割合 100.0%

ai_mmdfs_tt
ミニマックス法で計算したか True
計算したノードの数 2271
枝狩りしたノードの数 547675
合計 549946
割合 0.4%

ai_abs_tt
ミニマックス法で計算したか False
計算したノードの数 18297
枝狩りしたノードの数 531649
合計 549946
割合 3.3%

ai_abs_tt3
ミニマックス法で計算したか False
計算したノードの数 1173
枝狩りしたノードの数 548773
合計 549946
割合 0.2%

ai_abs_tt4
ミニマックス法で計算したか False
計算したノードの数 832
枝狩りしたノードの数 549114
合計 549946
割合 0.2%
```

## `calc_score_by_ab` の修正

初心者の方は忘れがちだと思いますが、上記で `calc_score_for_anim` で計算されたかどうかを表す __`calculated_by_calc_score_for_anim` という属性を導入__ したので、__`calc_score_by_ab`__ で計算処理を行った場合に __この属性に `False` を代入する処理を記述する必要__ があります。

その修正を忘れると __同じゲーム木のデータ__ に対して __`calc_score_for_anim` で計算を行なった後__ で __`calc_score_by_ab`__ で計算を行うと __`calculated_by_calc_score_for_anim`__ の値が __`True` のまま変化しない__ という問題が発生するからです。

実際にそのことは __下記のプログラムで確認__ できます。先程 `calc_score_for_anim` で計算を行なったので下記の実行結果の 1 行目では `calculated_by_calc_score_for_anim` の値として `True` が表示されますが、その後で __`calc_score_by_ab` で計算し直した後__ の 3 行目の __実行結果も `True` が表示__ されています。

```python
print(mbtree.calculated_by_calc_score_for_anim)
mbtree.calc_score_by_ab(mbtree.root)
print(mbtree.calculated_by_calc_score_for_anim)
```

実行結果

```
True
True
```

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

* __3 行目__：`calculated_by_calc_score_for_anim` に `False` を代入するように修正した

```python
1  def calc_score_by_ab(self, abroot, debug:bool=False, minimax=False, init_ab=False, use_tt=False):
元と同じなので省略
2      from ai import dprint       
3      self.calculated_by_calc_score_for_anim = False    
4      for node in self.nodelist:
5          node.score_index = float("inf")
6          node.pruned_index = float("inf")
元と同じなので省略 
7     
8  Mbtree.calc_score_by_ab = calc_score_by_ab
```

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

```python
def calc_score_by_ab(self, abroot, debug:bool=False, minimax=False, init_ab=False, use_tt=False):
    def assign_pruned_index(node, index):
        node.pruned_index = index
        self.num_pruned += 1
        for childnode in node.children:
            assign_pruned_index(childnode, index)
        
    def calc_ab_score(node, tt, alpha=float("-inf"), beta=float("inf")):
        if minimax:
            alpha = float("-inf")
            beta = float("inf")
        if use_tt:
            boardtxt = node.mb.board_to_str()
            key = (boardtxt, alpha, beta)
            if key in tt:
                node.score = tt[key]
                if node.mb.turn == Marubatsu.CIRCLE:
                    alpha = node.score
                else:
                    beta = node.score
                self.nodelist_by_score.append(node)
                self.num_calculated += 1     
                for childnode in node.children:
                    assign_pruned_index(childnode, len(self.nodelist_by_score) - 1)
                self.ablist_by_score.append((alpha, beta, None, "tt",
                                            self.num_calculated, self.num_pruned))
                node.score_index = len(self.nodelist_by_score) - 1  
                return node.score
                
        self.nodelist_by_score.append(node)
        self.ablist_by_score.append((alpha, beta, None, "start",
                                    self.num_calculated, self.num_pruned))
        if node.mb.status != Marubatsu.PLAYING:
            self.calc_score_of_node(node)
            if node.mb.turn == Marubatsu.CIRCLE:
                alpha = node.score
            else:
                beta = node.score
        else:
            if node.mb.turn == Marubatsu.CIRCLE:
                for childnode in node.children:
                    score = calc_ab_score(childnode, tt, alpha, beta)
                    self.nodelist_by_score.append(node)
                    self.ablist_by_score.append((alpha, beta, score, "score",
                                                self.num_calculated, self.num_pruned))
                    if score > alpha:
                        alpha = score
                    self.nodelist_by_score.append(node)
                    if score >= beta:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
                    self.ablist_by_score.append((alpha, beta, None, "update",
                                                self.num_calculated, self.num_pruned))
                    if score >= beta:
                        break
                node.score = alpha
            else:
                for childnode in node.children:
                    score = calc_ab_score(childnode, tt, alpha, beta)
                    self.nodelist_by_score.append(node)
                    self.ablist_by_score.append((alpha, beta, score, "score",
                                                self.num_calculated, self.num_pruned))
                    if score < beta:
                        beta = score
                    self.nodelist_by_score.append(node)
                    if score <= alpha:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
                    self.ablist_by_score.append((alpha, beta, None, "update",
                                                self.num_calculated, self.num_pruned))
                    if score <= alpha:
                        break
                node.score = beta

        self.nodelist_by_score.append(node)
        self.num_calculated += 1     
        self.ablist_by_score.append((alpha, beta, None, "end",
                                    self.num_calculated, self.num_pruned))
        node.score_index = len(self.nodelist_by_score) - 1  
        if use_tt:
            from util import calc_same_boardtexts   
            
            boardtxtlist = calc_same_boardtexts(node.mb)
            _, alpha, beta = key
            for boardtxt in boardtxtlist:
                tt[(boardtxt, alpha, beta)] = node.score            
        return node.score

    from ai import dprint       
    self.calculated_by_calc_score_for_anim = False    
    for node in self.nodelist:
        node.score_index = float("inf")
        node.pruned_index = float("inf")
    self.nodelist_by_score = []
    self.ablist_by_score = []
    self.num_calculated = 0
    self.num_pruned = 0
    if init_ab:
        alpha = -2 if self.shortest_victory else -1
        beta = 3 if self.shortest_victory else 1
    else:
        alpha = float("-inf")
        beta = float("inf")
    calc_ab_score(abroot, {}, alpha, beta)
    total_nodenum = self.num_pruned + self.num_calculated
    ratio = self.num_calculated / total_nodenum * 100
    dprint(debug, "ミニマックス法で計算したか", minimax)    
    dprint(debug, "計算したノードの数",  self.num_calculated)
    dprint(debug, "枝狩りしたノードの数",  self.num_pruned)
    dprint(debug, "合計",  total_nodenum)
    dprint(debug, f"割合 {ratio:.1f}%")
    
Mbtree.calc_score_by_ab = calc_score_by_ab
```
</details>

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

```diff_python
def calc_score_by_ab(self, abroot, debug:bool=False, minimax=False, init_ab=False, use_tt=False):
元と同じなので省略
    from ai import dprint       
+   self.calculated_by_calc_score_for_anim = False    
    for node in self.nodelist:
        node.score_index = float("inf")
        node.pruned_index = float("inf")
元と同じなので省略 

Mbtree.calc_score_by_ab = calc_score_by_ab
```
</details>

上記の修正後に下記のプログラムで改めて `calc_score_by_ab` を実行すると、実行結果のように __`calculated_by_calc_score_for_anim` 属性に `False` が代入されるようになった__ ことが確認できます。

```python
mbtree.calc_score_by_ab(mbtree.root)
print(mbtree.calculated_by_calc_score_for_anim)
```

実行結果

```
False
```

:::note
上記の修正を行っても、"../data/abtree_root.mbtree" のように、__修正前の `calc_score_by_ab` で計算したデータ__ には __`calculated_by_calc_score_for_anim` 属性が存在しない__ という問題があるのではないかと思った人がいるかもしれません。その点に関する対処は次回の記事で行います。
:::

# 今回の記事のまとめ

今回の記事では、__`calc_score_for_anim`__ で __ミニマックス法で評価値を計算したデータ__ を Mbtree_Anim で __視覚化する方法について検討__ し、そのために __必要なデータを記録する__ ように __`calc_score_for_anim` を修正__ しました。

次回の記事では Mbtree_Anim を修正して実際の視覚化を行うことができるようにします。

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

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

# 次回の記事

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

[^1]:詳細は省略しますが、Mbtree_Anim クラスを継承して、処理の内容が異なるメソッドだけを定義することで実装を比較的簡単に行うという方法も考えられますが、そのような継承はあまり推奨されていない方法だと思います
[^2]:この情報が必要になるのは、最短の勝利を目指すかによって数直線の表示範囲を変えるようにしたからです
[^3]:`calc_score_by_ab` では正しく記述していたのを、`calc_score_for_anim` に修正する際に間違った場所に記述してしまいました