Pythonで〇×ゲームのAIを一から作成する　その62　条件の統合と乱数の種によるプログラムの再現性

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/70c50f75319373399871

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

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

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

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

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

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

| ルール | アルゴリズム |
|:-:|:--|
| __[ルール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)__  | __左上から順__ に __空いているマス__ を探し、__最初に見つかったマス__ に __着手__ する |
| __[ルール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)__  | __ランダム__ なマスに __着手__ する |
| __[ルール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)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>既に __埋まっていた場合__ は __ランダム__ なマスに __着手__ する | |
| __[ルール4](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#%E3%83%AB%E3%83%BC%E3%83%AB-4%E5%9B%9B%E9%9A%85%E3%81%AE%E3%83%9E%E3%82%B9%E3%81%AB%E5%84%AA%E5%85%88%E3%81%97%E3%81%A6%E7%9D%80%E6%89%8B%E3%81%99%E3%82%8B)__  | __真ん中__、__隅__ のマスの __順__ で __優先的__ に __着手__ する<br>既に __埋まっていた場合__ は __ランダム__ なマスに __着手__ する | あり |
| __[ルール5](https://qiita.com/ysgeso/items/ff28a95dff50322e6b16#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-5%E5%8B%9D%E3%81%A6%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AB%E5%8B%9D%E3%81%A4)__  | __勝てる場合__ に __勝つ__<br>そうでない場合は __ランダム__ なマスに __着手__ する | |
| __[ルール6](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-6%E7%9B%B8%E6%89%8B%E3%81%AE%E5%8B%9D%E5%88%A9%E3%82%92%E9%98%BB%E6%AD%A2%E3%81%99%E3%82%8B)__  | __勝てる場合__ に __勝つ__<br>そうでない場合は __相手の勝利__ を __阻止__ する<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール6改](https://qiita.com/ysgeso/items/6c0d73cc9ef4efd62d48#%E3%83%AB%E3%83%BC%E3%83%AB-6-%E6%94%B9%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __勝てる場合__ に __勝つ__<br>そうでない場合は __相手__ が __勝利できる__ 着手を __行わない__<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール7](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-7%E3%83%AB%E3%83%BC%E3%83%AB3-%E3%81%A8-%E3%83%AB%E3%83%BC%E3%83%AB-6-%E3%81%AE%E4%BD%B5%E7%94%A8)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手の勝利__ を __阻止__ する<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール7改](https://qiita.com/ysgeso/items/6c0d73cc9ef4efd62d48#%E8%A9%95%E4%BE%A1%E5%80%A4%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F-%E3%83%AB%E3%83%BC%E3%83%AB-7-%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)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手__ が __勝利できる__ 着手を __行わない__<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール8](https://qiita.com/ysgeso/items/58754ba98c286f414ea4#%E3%83%AB%E3%83%BC%E3%83%AB-8-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手__ が __勝利できる__ 着手を __行わない__<br>そうでない場合は、__次__ の __自分の手番__ で __勝利できる__ ように、「__自 2 敵 0 空 1__」が 1 つ以上 __存在する__ 局面になる着手を行う<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール9](https://qiita.com/ysgeso/items/c1285c93cbfbdbdc7b77#%E3%83%AB%E3%83%BC%E3%83%AB-9-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手__ が __勝利できる__ 着手を __行わない__<br>そうでない場合は、__次__ の __自分の手番__ で __必ず勝利できる__ ように、「__自 2 敵 0 空 1__」が __2 つ以上存在する__ 局面になる着手を行う<br>そうでない場合は、__次__ の __自分の手番__ で __勝利できる__ ように、「__自 2 敵 0 空 1__」が __1 つ存在する__ 局面になる着手を行う<br>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール10](https://qiita.com/ysgeso/items/c1285c93cbfbdbdc7b77#%E3%83%AB%E3%83%BC%E3%83%AB-10)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手__ が __勝利できる__ 着手を __行わない__<br>そうでない場合は、__次__ の __自分の手番__ で __必ず勝利できる__ ように、「__自 2 敵 0 空 1__」が __2 つ以上存在する__ 局面になる着手を行う<br>そうでない場合は、__以下__ の 2 つを __総合的に判断__ して着手を行う<ul><li>__次__ の __自分の手番__ で __勝利できる__ ように、「__自 2 敵 0 空 1__」が __1 つ存在する__ 局面になる着手を行う</li><li>__自分__ が __有利になる__ ように、「__自 1 敵 0 空 2__」が __最も多い__ 着手を行う</li></ul>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール11](https://qiita.com/ysgeso/items/c1285c93cbfbdbdc7b77#%E3%83%AB%E3%83%BC%E3%83%AB-11)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手__ が __勝利できる__ 着手を __行わない__<br>そうでない場合は、__次__ の __自分の手番__ で __必ず勝利できる__ ように、「__自 2 敵 0 空 1__」が __2 つ以上存在する__ 局面になる着手を行う<br>そうでない場合は、__以下__ の 3 つを __総合的に判断__ して着手を行う<ul><li>__次__ の __自分の手番__ で __勝利できる__ ように、「__自 2 敵 0 空 1__」が __1 つ存在する__ 局面になる着手を行う</li><li>__自分__ が __有利になる__ ように、「__自 1 敵 0 空 2__」が __最も多い__ 着手を行う</li><li>__相手__ が __不利になる__ ように、「__自 0 敵 1 空 2__」が __最も少ない__ 着手を行う</li></ul>そうでない場合は __ランダム__ なマスに __着手__ する |
| __[ルール12](https://qiita.com/ysgeso/items/70c50f75319373399871#%E9%99%90%E5%AE%9A%E7%9A%84%E3%81%AA%E6%9D%A1%E4%BB%B6%E3%81%AE%E8%BF%BD%E5%8A%A0%E3%81%AB%E3%82%88%E3%82%8B%E3%83%AB%E3%83%BC%E3%83%AB-12-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __真ん中__ のマスに __優先的__ に __着手__ する<br>そうでない場合は __勝てる場合__ に __勝つ__<br>そうでない場合は __相手__ が __勝利できる__ 着手を __行わない__<br>そうでない場合は、__次__ の __自分の手番__ で __必ず勝利できる__ ように、「__自 2 敵 0 空 1__」が __2 つ以上存在する__ 局面になる着手を行う<br>そうでない場合は、__斜め方向__ に __〇×〇__ が __並び__、__他の 6 マス__ が __空のマス__ の場合に、__いずれか__ の __辺のマス__ に着手を行う<br>そうでない場合は、__以下__ の 3 つを __総合的に判断__ して着手を行う<br><ul><li>__次__ の __自分の手番__ で __勝利できる__ ように、「__自 2 敵 0 空 1__」が __1 つ存在する__ 局面になる着手を行う</li><li>__自分__ が __有利になる__ ように、「__自 1 敵 0 空 2__」が __最も多い__ 着手を行う</li><li>__相手__ が __不利になる__ ように、「__自 0 敵 1 空 2__」が __最も少ない__ 着手を行う</li></ul>そうでない場合は __ランダム__ なマスに __着手__ する |

__ルール 11、12__ で __評価値を計算__ する際の __パラメータ__ は以下の通りです。

|  | `ai11s`<br>ver 1 | `ai11s`<br> ver 2 | `ai11s` ver 3<br>`ai12s` |
|:-:|:-:|:-:|:-:|
| __「自 2 敵 0 空 1」が 1 つの場合の評価値__  | 1  | 2 | 2 |
| __「自 1 敵 0 空 2」が 1 つあたりの評価値__ | 1  | 1 | 0.5 |
| __「自 0 敵 1 空 2」が 1 つあたりの評価値__ | -1 | -1 | 1 |


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

| 関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 | 欠陥 |
|:-:|--:|--:|--:|--:|--:|--:|--:|--:|--:|:-:|
| __[`ai1`](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#ai1-%E3%81%AE%E5%AE%9A%E7%BE%A9)__<br>__[`ai1s`](https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf#ai1s-%E3%81%AE%E4%BF%AE%E6%AD%A3)__  | __78.1__ | __17.5__ | 4.4 | __44.7__ | __51.6__ | 3.8 | __61.4__ | __34.5__ | 4.1 | あり |
| __[`ai2`](https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58#ai2-%E3%81%AE%E5%AE%9A%E7%BE%A9)__<br>__[`ai2s`](https://qiita.com/ysgeso/items/8f3d4b3d2c02ef51a70e#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%A2%E6%95%B0%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E3%83%AB%E3%83%BC%E3%83%AB-2-%E3%81%A7%E7%9D%80%E6%89%8B%E3%82%92%E8%A1%8C%E3%81%86-ai-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | 58.7 | 28.8 | 12.6 | 29.1 | 58.6 | 12.3 | 43.9 | 43.7 | 12.5 | |
| __[`ai3`](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#ai3-%E3%81%AE%E5%AE%9F%E8%A3%85)__<br>__[`ai3s`](https://qiita.com/ysgeso/items/8f3d4b3d2c02ef51a70e#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E9%96%A2%E6%95%B0)__  | __69.3__ | __19.2__ | 11.5 | __38.9__ | __47.6__ | 13.5 | __54.1__ | __33.4__ | 12.5 | |
| __[`ai4`](https://qiita.com/ysgeso/items/1e8732db6cd1b0104276#%E3%83%AB%E3%83%BC%E3%83%AB-4%E5%9B%9B%E9%9A%85%E3%81%AE%E3%83%9E%E3%82%B9%E3%81%AB%E5%84%AA%E5%85%88%E3%81%97%E3%81%A6%E7%9D%80%E6%89%8B%E3%81%99%E3%82%8B)__<br>__[`ai4s`](https://qiita.com/ysgeso/items/3afe5630d7e615a28ebf#ai4s-%E3%81%AE%E4%BF%AE%E6%AD%A3%E3%81%A8%E5%8B%95%E4%BD%9C%E3%81%AE%E7%A2%BA%E8%AA%8D)__  | __83.0__ | __9.5__ | 7.4 | __57.2__ | __33.0__ | 9.7 | __70.1__ | __21.3__ | 8.6 | あり |
| __[`ai5`](https://qiita.com/ysgeso/items/ff28a95dff50322e6b16#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-5%E5%8B%9D%E3%81%A6%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AB%E5%8B%9D%E3%81%A4)__<br>__[`ai5s`](https://qiita.com/ysgeso/items/6c0d73cc9ef4efd62d48#ai5s-%E3%81%AE%E5%AE%9F%E8%A3%85)__  | __81.2__ | __12.3__ | 6.5 | __51.8__ | __39.8__ | 8.4 | __66.5__ | __26.0__ | 7.4 | |
| __[`ai6`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-6%E7%9B%B8%E6%89%8B%E3%81%AE%E5%8B%9D%E5%88%A9%E3%82%92%E9%98%BB%E6%AD%A2%E3%81%99%E3%82%8B)__  | __88.9__ | __2.2__ | 8.9 | __70.3__ | __6.2__ | 23.5 | __79.6__ | __4.2__ | 16.2 |
| __[`ai6s`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-6%E7%9B%B8%E6%89%8B%E3%81%AE%E5%8B%9D%E5%88%A9%E3%82%92%E9%98%BB%E6%AD%A2%E3%81%99%E3%82%8B)__  |   __88.6__ | __1.9__ | 9.5 | __69.4__ | __9.1__ | 21.5 | __79.0__ | __5.5__ | 15.5 |
| __[`ai7`](https://qiita.com/ysgeso/items/e08c41f93109496fb9f9#%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%9D%E3%81%AE-7%E3%83%AB%E3%83%BC%E3%83%AB3-%E3%81%A8-%E3%83%AB%E3%83%BC%E3%83%AB-6-%E3%81%AE%E4%BD%B5%E7%94%A8)__<br>__[`ai7s`](https://qiita.com/ysgeso/items/6c0d73cc9ef4efd62d48#ai7s-%E3%81%AE%E5%AE%9A%E7%BE%A9)__ | __95.8__ | __0.2__ | 4.0 | __82.3__ | __2.4__ | 15.3 | __89.0__ | __1.3__ | 9.7 |
| __[`ai8s`](https://qiita.com/ysgeso/items/085fd15fbd91d5cdf770#ai8s-%E3%81%AE%E4%BF%AE%E6%AD%A3)__  | __98.2__ | __0.1__ | 1.6 | __89.4__ | __2.5__ | 8.1 | __93.8__ | __1.3__ | 4.9 |
| __[`ai9s`](https://qiita.com/ysgeso/items/c1285c93cbfbdbdc7b77#ai9s-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __98.7__ | __0.1__ | 1.2 | __89.6__ | __2.4__ | 8.0 | __94.1__ | __1.3__ | 4.6 |
| __[`ai10s`](https://qiita.com/ysgeso/items/c1285c93cbfbdbdc7b77#ai10s-%E3%81%AE%E5%AE%9A%E7%BE%A9)__  | __97.4__ | __0.0__ | 2.6 | __85.6__ | __2.6__ | 11.7 | __91.5__ | __1.3__ | 7.2 |
| __[`ai11s` ver 1](https://qiita.com/ysgeso/items/c1285c93cbfbdbdc7b77#ai11s-%E3%81%AE%E5%AE%9A%E7%BE%A9%E3%81%A8ai2ai10s-%E3%81%A8%E3%81%AE%E5%AF%BE%E6%88%A6)__  | __98.1__ | __0.0__ | 1.9 | __82.5__ | __1.9__ | 15.6 | __90.3__ | __1.0__ | 8.7 | あり |
| __[`ai11s` ver 2](https://qiita.com/ysgeso/items/5fe28ae6c765a169fd34#ai11s-%E3%81%AE%E4%BF%AE%E6%AD%A3)__ | __98.8__ | __0.0__ | 1.2 | __87.7__ | __2.4__ | 10.0 | __93.2__ | __1.2__ | 5.6 | |
| __[`ai11s` ver 3](https://qiita.com/ysgeso/items/70c50f75319373399871#%E4%BF%AE%E6%AD%A3%E3%81%97%E3%81%9F%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%A7%E3%81%AE-ai2-%E3%81%A8%E3%81%AE%E5%AF%BE%E6%88%A6)__  | __99.1__ | __0.0__ | 0.9 | __87.7__ | __0.8__ | 11.5 | __93.4__ | __0.4__ | 6.2 | |
| __[`ai12s`](https://qiita.com/ysgeso/items/70c50f75319373399871#ai12s-%E3%81%AE%E5%AE%9A%E7%BE%A9-1)__  | __98.9__ | __0.0__ | 1.1 | __88.2__ | __0.0__ | 11.8 | __93.5__ | __0.0__ | 6.5 | |

# 前回の記事のおさらい

前回の記事で、__ルールベースの AI__ として、__`ai12s`__ という、__弱解決の AI__ が完成しました。

今回の記事から、__ルールベースの AI__ に関するいくつかの __補足説明__ を行います。

# ルールの条件の統合

__ルールベースの AI__ は、__ルール__ の __条件__ が増えれば __増えるほど__、__複雑になる__ ため、__わかりづらく__ なったり、__複数の条件__ の間で __矛盾が生じたりする__ ようになります。そのため、なるべく __条件の数__ が __少ない__ ほうが __望ましい__ と言えるので、__複数の条件__ の中で、__一つにまとめる__ ことが __できる__ ものがあれば、それらを __統合__ したほうが __良いでしょう__。

## ルール 12 の条件の統合

実は、__ルール 12__ の __条件__ の中の、最初の「__真ん中__ のマスに __優先的__ に __着手__ する」という __条件__ は、__ルール 12__ の __下記の条件__ があれば __削除__ しても __真ん中__ のマスに __優先的__ に __着手__ が __行われる__ ので、__下記の条件__ に __統合__ することが __できます__。

* __次__ の __自分の手番__ で __勝利できる__ ように、「__自 2 敵 0 空 1__」が __1 つ存在する__ 局面になる着手を行う
* __自分__ が __有利になる__ ように、「__自 1 敵 0 空 2__」が __最も多い__ 着手を行う
* __相手__ が __不利になる__ ように、「__自 0 敵 1 空 2__」が __最も少ない__ 着手を行う

__統合できる理由__ について __説明__ します。

## `ai12s` の修正と確認

まず、__下記__ のプログラムのように、__`ai12s`__ から、「__真ん中__ のマスに __優先的__ に __着手__ する」という __条件を処理__ する __部分を削除__ します。以後は、__修正前__ の __`ai12s`__ を __`ai12s` ver1__、__修正後__ を __`ai12s` ver 2__ と __表記__ することにします。

```python
from marubatsu import Markpat, Marubatsu
from ai import ai_by_score
from pprint import pprint

def ai12s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):    
    def eval_func(mb):      
        # ここにあった、真ん中のマスに着手した場合に評価値として 400 を返す処理を削除する
        
        # 自分が勝利している場合は、評価値として 300 を返す
        if mb.status == mb.last_turn:
            return 300
元と同じなので省略
```

<details><summary><strong>プログラム全体</strong></summary>

```python
from marubatsu import Markpat, Marubatsu
from ai import ai_by_score
from pprint import pprint

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

        markpats = mb.count_markpats()
        if debug:
            pprint(markpats)
        # 相手が勝利できる場合は評価値として -100 を返す
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
            return -100
        # 次の自分の手番で自分が必ず勝利できる場合は評価値として 200 を返す
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
            return 200
        
        # 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が
        # 配置されている場合は評価値として 100 を返す
        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 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

    return ai_by_score(mb, eval_func, debug=debug)
```
</details>

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

```diff_python
from marubatsu import Markpat, Marubatsu
from ai import ai_by_score
from pprint import pprint

def ai12s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):    
    def eval_func(mb):      
        # 真ん中のマスに着手している場合は、評価値として 400 を返す
-       if mb.last_move == (1, 1):
-           return 400
            
        # 自分が勝利している場合は、評価値として 300 を返す
        if mb.status == mb.last_turn:
            return 300
元と同じなので省略
```
</details>

次に、__`ai12s` ver 2__ が、__真ん中のマス__ に __優先的__ に __着手を行う__ ことを __検証__ します。そのためには、__〇 の手番__ と __× の手番__ の __それぞれを担当__ する場合の __最初の手番__ である、__ゲーム開始時__ の __局面__ と、__2 手目__ の __局面__ について __調べる必要__ が __あります__。

## ゲーム開始時の局面での `ai12s` ver 2 の合法手の選択の検証

__ゲーム開始時__ に __`ai12s` ver 2__ が __選択__ する __合法手__ を __下記__ のプログラムで __検証__ します。

```python
mb = Marubatsu()
ai12s(mb, debug=True)
```

<details><summary><strong>実行結果（長いのでクリックして開いてください）</strong></summary>

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

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 6,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score 1.0 best score 1.5
====================
move (2, 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 1.5
APPEND
  best moves [(0, 0), (2, 0)]
====================
move (0, 1)
Turn x
...
O..
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 6,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score 1.0 best score 1.5
====================
move (1, 1)
Turn x
...
.O.
...

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

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 6,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score 1.0 best score 2.0
====================
move (0, 2)
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 2.0
====================
move (1, 2)
Turn x
...
...
.O.

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 6,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score 1.0 best score 2.0
====================
move (2, 2)
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 2.0
====================
Finished
best score 2.0
best moves [(1, 1)]
```

</details>

下記は、__上記の結果__ を __まとめた表__ です。なお、表の __それぞれの合法手__ には __1 つの局面__ の __図__ しか __表記__ していませんが、__合法手 2__ と __3__  にはそれぞれ __4 つの同一局面__ が __存在__ します。

| |  | 「201」 | 「021」 | 「102」 | 「012」 | 評価値 |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| | __評価値__ | 1：`+2`<br>2~：`200` | `-100` | 1 つで<br>`+0.5` | 1 つで<br>`-1` | |
| 1 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d329893a-5ee1-4bdc-c74a-1f8c9c3b672b.png" width="65">  |  | | 4 |  | 2 |
| 2 | 隅のマス<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/dbfdee77-2c96-3b61-b576-371c416b1562.png" width="65">  |  | | 3 |  | 1.5 |
| 3 | 辺のマス<br><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b1f9ef71-1987-f93a-66c0-cfc229e304ff.png" width="65">  |  | | 2 |  | 1 |

__上記の表__ から、__真ん中__ の __(1, 1)__ のマスを表す __合法手 1__ の __評価値__ が __2__ で __最も高くなる__ ことがわかります。そのことは、下記の __実行結果__ の __最後の 2 行__ からも __確認__ できます。

```
best score 2.0
best moves [(1, 1)]
```

また、__下記の性質__ から、__`ai12s` ver 2__ の __パラメータ__ を __どのように設定__ しても、__真ん中のマスを__ 表す __合法手 1__ の __評価値のみ__ が __最も高くなる__ ことが __わかります__。

* __3 つの合法手__ はいずれも、「__自 1 敵 0 空 2__」の __マークのパターンのみ__ が __存在__ する
* 「__自 1 敵 0 空 2__」の __マークのパターン__ の __数__ は、__合法手 1 のみ__ が __最も大きい__
* [以前の記事](https://qiita.com/ysgeso/items/fc19eb2764e0766b59d5#%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%AE%E8%AA%BF%E6%95%B4%E3%81%AE%E3%81%95%E3%82%89%E3%81%AA%E3%82%8B%E6%B3%A8%E6%84%8F%E7%82%B9-%E3%81%9D%E3%81%AE-1) で説明したように、「__自 1 敵 0 空 2__」の __マークのパターン__ の __パラメータ__ は、__正の値__ である __必要がある__

上記から、__`ai12s` ver 2__ は、__〇__ の __最初の手番__ である __ゲーム開始時__ の __局面__ で、__真ん中のマス__ に __優先的__ に __着手を行う__ ことが __確認__ できました。

## 2 手目の局面での `ai12s` ver 2 の合法手の選択の検証

__2 手目の局面__ は、__同一局面__ を __考慮__ すると、下記の __3 種類__ の __局面__ が __あります__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d329893a-5ee1-4bdc-c74a-1f8c9c3b672b.png" width="65"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/dbfdee77-2c96-3b61-b576-371c416b1562.png" width="65"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b1f9ef71-1987-f93a-66c0-cfc229e304ff.png" width="65">

このうち、__左の局面__ は、__真ん中__ のマスに __マークが配置済__ なので __検証する必要__ は __ありません__ ので、__残りの 2 つ__ の局面で __検証__ を行います。

### 1 手目で隅のマスに 〇 が配置された場合

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/dbfdee77-2c96-3b61-b576-371c416b1562.png" width="65">

まず、__下記__ のプログラムで、__1 手目__ で __隅のマス__ に __〇 が配置__ された場合を __検証__ します。

```python
mb = Marubatsu()
mb.move(0, 0)
ai12s(mb, debug=True)
```

<details><summary><strong>実行結果（長いのでクリックして開いてください）</strong></summary>

```
Start ai_by_score
Turn x
O..
...
...

legal_moves [(1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
====================
move (1, 0)
Turn o
oX.
...
...

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

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -1.0 best score -1.5
UPDATE
  best score -1.0
  best moves [(2, 0)]
====================
move (0, 1)
Turn o
o..
X..
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 1,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -1.5 best score -1.0
====================
move (1, 1)
Turn o
o..
.X.
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 2,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 3,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -0.5 best score -1.0
UPDATE
  best score -0.5
  best moves [(1, 1)]
====================
move (2, 1)
Turn o
o..
..X
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 3,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score -2.0 best score -0.5
====================
move (0, 2)
Turn o
o..
...
X..

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -1.0 best score -0.5
====================
move (1, 2)
Turn o
o..
...
.X.

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 3,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score -2.0 best score -0.5
====================
move (2, 2)
Turn o
o..
...
..X

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -1.0 best score -0.5
====================
Finished
best score -0.5
best moves [(1, 1)]
```

</details>

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

| |  | 「201」 | 「021」 | 「102」 | 「012」 | 評価値 |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| | __評価値__ | 1：`+2`<br>2~：`200` | `-100` | 1 つで<br>`+0.5` | 1 つで<br>`-1` | |
| 1 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/94783165-9c79-fdd1-7c83-18e5d7900b2c.png" width="140">  |  | | 1 | 2 | -1.5 |
| 2 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a0c9d31a-c6aa-5c1e-6b49-a82e235e0263.png" width="140">  |  | | 2 | 2 | -1 |
| 3 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/3124118a-45c8-4b7e-c6bb-9058aa043176.png" width="65">  |  | | 3 | 2 | -0.5 |
| 4 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/10b39fd9-8536-f6be-7e45-ce0176e74647.png" width="140">  |  | | 2 | 3 | -2 |
| 5 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/a3bff67e-fab1-a68a-400e-5d4522563094.png" width="65">  |  | | 2 | 2 | -1 |

__上記の表__ から、__真ん中__ の __(1, 1)__ のマスを表す __合法手 3__ の __評価値__ が __-0.5__ で __最も高くなる__ ことが __わかります__。そのことは、下記の __実行結果__ の __最後の 2 行__ からも __確認__ できます。

```
best score -0.5
best moves [(1, 1)]
```

また、__下記の性質__ から、__`ai12s` ver 2__ の __パラメータ__ を __どのように設定__ しても、__真ん中のマスを__ 表す __合法手 3__ の __評価値のみ__ が __最も高くなる__ ことが __わかります__。

* __3 つ__ の __合法手__ は __いずれも__、「__自 1 敵 0 空 2__」と「__自 0 敵 1 空 2__」の __マークのパターンのみ__ が __存在__ する
* 「__自 1 敵 0 空 2__」の __マークのパターン__ の __数__ は、__合法手 3 のみ__ が __最も大きい__
* [以前の記事](https://qiita.com/ysgeso/items/fc19eb2764e0766b59d5#%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%AE%E8%AA%BF%E6%95%B4%E3%81%AE%E3%81%95%E3%82%89%E3%81%AA%E3%82%8B%E6%B3%A8%E6%84%8F%E7%82%B9-%E3%81%9D%E3%81%AE-1) で説明したように、「__自 1 敵 0 空 2__」の __マークのパターン__ の __パラメータ__ は、__正の値__ である __必要がある__
* 「__自 0 敵 1 空 2__」の __マークのパターン__ の __数__ は、__合法手 3__ が __最も小さい__
* [以前の記事](https://qiita.com/ysgeso/items/70c50f75319373399871#%E6%95%97%E7%8E%87%E3%81%8C%E4%B8%8A%E6%98%87%E3%81%97%E3%81%9F%E7%90%86%E7%94%B1) で説明したように、「__自 0 敵 1 空 2__」の __マークのパターン__ の __パラメータ__ は、__負の値__ である __必要がある__

:::note
__上記の性質__ の中で、「__自 1 敵 0 空 2__」の __マークのパターン__ の __数__ が __最大__ になる __合法手__ が、__合法手 3 のみ__ であるという __性質__ が __特に重要__ です。__この性質__ が __なければ__、__合法手 3 以外__ の __合法手__ も __評価値が最大__ になる __可能性が生じる__ からです。
:::

### 1 手目で辺のマスに 〇 が配置された場合

次に、__下記__ のプログラムで、__1 手目__ で __辺のマス__ に __〇 が配置__ された場合を __検証__ します。

```python
mb = Marubatsu()
mb.move(1, 0)
ai12s(mb, debug=True)
```

<details><summary><strong>実行結果（長いのでクリックして開いてください）</strong></summary>

```
Start ai_by_score
Turn x
.O.
...
...

legal_moves [(0, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
====================
move (0, 0)
Turn o
Xo.
...
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 1,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score 0.0 best score -inf
UPDATE
  best score 0.0
  best moves [(0, 0)]
====================
move (2, 0)
Turn o
.oX
...
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 1,
             Markpat(last_turn=1, turn=0, empty=2): 2,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score 0.0 best score 0.0
APPEND
  best moves [(0, 0), (2, 0)]
====================
move (0, 1)
Turn o
.o.
X..
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score -1.0 best score 0.0
====================
move (1, 1)
Turn o
.o.
.X.
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 1,
             Markpat(last_turn=1, turn=0, empty=2): 3,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score 0.5 best score 0.0
UPDATE
  best score 0.5
  best moves [(1, 1)]
====================
move (2, 1)
Turn o
.o.
..X
...

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 4,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 2})
score -1.0 best score 0.5
====================
move (0, 2)
Turn o
.o.
...
X..

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 3})
score -0.5 best score 0.5
====================
move (1, 2)
Turn o
.o.
...
.X.

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 5,
             Markpat(last_turn=0, turn=1, empty=2): 1,
             Markpat(last_turn=1, turn=0, empty=2): 1,
             Markpat(last_turn=1, turn=1, empty=1): 1})
score -0.5 best score 0.5
====================
move (2, 2)
Turn o
.o.
...
..X

defaultdict(<class 'int'>,
            {Markpat(last_turn=0, turn=0, empty=3): 3,
             Markpat(last_turn=0, turn=1, empty=2): 2,
             Markpat(last_turn=1, turn=0, empty=2): 3})
score -0.5 best score 0.5
====================
Finished
best score 0.5
best moves [(1, 1)]
```

</details>

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

| |  | 「201」 | 「021」 | 「102」 | 「012」 | 評価値 |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| | __評価値__ | 1：`+2`<br>2~：`200` | `-100` | 1 つで<br>`+0.5` | 1 つで<br>`-1` | |
| 1 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1fc4dea4-a269-a2e2-e8b8-9ff9074b9a12.png" width="140">  |  | | 2 | 1 | 0 |
| 2 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b57af945-dec7-43eb-d74d-04b371c8a7b8.png" width="140">  |  | | 2 | 2 | -1 |
| 3 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/752fea2b-1484-98ad-4d6b-f3cc0221fca8.png" width="65">  |  | | 3 | 1 | 0.5 |
| 4 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/5ecb829b-9910-1bd3-67b2-0158b2f70b9d.png" width="140">  |  | | 3 | 2 | -0.5 |
| 5 | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/008638b0-42f3-70f9-06ff-73301d0b1774.png" width="65">  |  | | 1 | 1 | -0.5 |

__上記の表__ から、__真ん中__ の __(1, 1)__ のマスを表す __合法手 3__ の __評価値__ が __0.5__ で __最も高くなる__ ことがわかります。そのことは、下記の __実行結果__ の __最後の 2 行__ からも __確認__ できます。

```
best score 0.5
best moves [(1, 1)]
```

また、__下記の性質__ から、__`ai12s` ver 2__ の __パラメータ__ を __どのように設定__ しても、__真ん中のマスを__ 表す __合法手 3__ の __評価値のみ__ が __最も高くなる__ ことが __わかります__。

* __3 つ__ の __合法手__ は __いずれも__、「__自 1 敵 0 空 2__」と「__自 0 敵 1 空 2__」の __マークのパターンのみ__ が __存在__ する
* 「__自 1 敵 0 空 2__」の __マークのパターン__ の __数__ は、__合法手 3__ が __最も大きい__
* [以前の記事](https://qiita.com/ysgeso/items/fc19eb2764e0766b59d5#%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%AE%E8%AA%BF%E6%95%B4%E3%81%AE%E3%81%95%E3%82%89%E3%81%AA%E3%82%8B%E6%B3%A8%E6%84%8F%E7%82%B9-%E3%81%9D%E3%81%AE-1) で説明したように、「__自 1 敵 0 空 2__」の __マークのパターン__ の __パラメータ__ は、__正の値__ である __必要がある__
* 「__自 0 敵 1 空 2__」の __マークのパターン__ の __数__ は、__合法手 3__ が __最も小さい__
* [以前の記事](https://qiita.com/ysgeso/items/70c50f75319373399871#%E6%95%97%E7%8E%87%E3%81%8C%E4%B8%8A%E6%98%87%E3%81%97%E3%81%9F%E7%90%86%E7%94%B1) で説明したように、「__自 0 敵 1 空 2__」の __マークのパターン__ の __パラメータ__ は、__負の値__ である __必要がある__
* 「__自 1 敵 0 空 2__」の __マークのパターン__ の __数__ が __最大__ で、なおかつ「__自 0 敵 1 空 2__」の __マークのパターン__ の __数__ が __最小__ なのは、__合法手 3 のみ__ である

:::note
__上記の性質__ は、先程の「__1 手目__ で __隅のマス__ に __〇 が配置__ された場合」の __性質__ と __似ているように見える__ かもしれませんが、__下記__ の点で __異なります__。

* 「__自 1 敵 0 空 2__」の __マークのパターン__ の __数__ が __最大__ である __合法手__ が __複数ある__
* __最後の性質__ が __追加__ されている

__最後の性質__ を __追加__ したのは、__この性質__ が __なければ__、__合法手 3 以外__ の __合法手__ も __評価値が最大__ になる __可能性が生じる__ からです。
:::

__以上__ の __検証__ から、__`ai12s` ver 2__ は、__× の手番__ である __2 手目__ の __局面__ で、__真ん中__ の __マス__ に __優先的__ に __着手を行う__ ことが __確認__ できました。

先程示したように、__1 手目の局面__ でも __真ん中__ の __マス__ に __優先的__ に __着手を行う__ ので、__`ai12s` ver 2__ は、__常に真ん中のマス__ に __優先的__ に __着手を行う__ ことが __わかりました__。

従って、__ルール 12__ から「__真ん中__ のマスに __優先的__ に __着手__ する」という __条件__ を __削除しても__、__同じ合法手__ が __選択される__ ので、__この条件__ は __削除__ しても __かまいません__。

## ルール 12 改の定義

__ルール 12__ から「__真ん中__ のマスに __優先的__ に __着手__ する」という __条件__ を __削除__ した、下記のルールを、__ルール 12 改__ と __定義__ する事にします。

| 順位 | 条件 | 種類 |
| :-:|:--|:--|
| 1 | __勝てる場合__ に __勝つ__  | 十分条件  |
| 2 | __相手__ が __勝利できる__ 着手を __行わない__  | 必要条件  |
| 3 | 「__自 2 敵 0 空 1__」が __2 つ以上存在__ する着手を行う  | 十分条件  |
| 4 | __斜め方向__ に __〇×〇__ が __並び__、__他の 6 マス__ が __空のマス__ の<br>場合は、__いずれか__ の __辺のマス__ に着手を行う  | 十分条件  |
| 5 | 「__自 2 敵 0 空 1__」が __1 つ存在__ する着手を行う<br>「__自 1 敵 0 空 2__」が __最も多い__ 着手を行う<br>「__自 0 敵 1 空 2__」が __最も少ない__ 着手を行う |
| 6 | __ランダム__ なマスに __着手__ する  |   |

また、__それぞれ__ の __条件__ に対する __評価値__ は __下記__ のようになります。

| 順位 | 局面の状況 | 個別 | 評価値 | 
|:-:|:--|:-:|:-:|
| 1 | __自分が勝利している__  | | `300` |
| 3 | __「自 2 敵 0 空 1」が 2 つ以上存在する__  | | `200` |
| 4 | __片方__ の __斜め方向__ に __〇×〇__ が __並び__、__いずれか__ の<br>__辺__ の __1 つのマスのみ__ に __× が配置__ されている  | | `100` |
| 5 | __「自 2 敵 0 空 1」が 1 つ存在する__<br>__「自 1 敵 0 空 2」が x 個存在する__<br>__「自 0 敵 1 空 2」が y 個存在する__| `2` (`0～2`)<br>`0.5 * x` (`0～4`)<br>`-y` (`-8~0`) | `-8～6`|
| 2 | __相手が勝利できる__  | | `-100` |

## `ai12s` ver 2 が弱解決の AI であることの確認

__`ai12s` ver 2__ が __弱解決の AI__ であることを __確認__ するために、__下記__ のプログラムで、__ランダムな着手__ を行う __`ai2`__ と __対戦__ させてみます。

```python
from ai import ai_match, ai2

ai_match(ai=[ai12s, ai2])
```

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

```
ai12s VS ai2
count     win    lose    draw
o        9905       0      95
x        8803       0    1197
total   18708       0    1292

ratio     win    lose    draw
o       99.1%    0.0%    0.9%
x       88.0%    0.0%   12.0%
total   93.5%    0.0%    6.5%
```

__実行結果__ から、__敗率__ が __0 %__ であることが __確認__ できたので、__`ai12s` ver 2__ は __弱解決の AI__ である __可能性が高い__ ことが __わかりました__。

下記は __上記__ の __対戦結果__ に、__`ai12s` ver 1 VS `ai2`__ の __対戦結果__ を __加えた__ ものです。__対戦成績__ は __ほぼ同じ__ なので、__`ai12s` ver 1__ と __`ai12s` ver 2__ は __同じ合法手__ を __選択__ する __AI__ である __可能性__ が __非常に高い__ ことが __確認__ できました。

| 関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 |
|:-:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| __`ai12s` ver 1__  | __98.9__ | __0.0__ | 1.1 | __88.2__ | __0.0__ | 11.8 | __93.5__ | __0.0__ | 6.5 |
| __`ai12s` ver 2__  | __99.1__ | __0.0__ | 0.9 | __88.0__ | __0.0__ | 12.0 | __93.5__ | __0.0__ | 6.5 |

# 最善手の優劣

これまでの説明では、__局面__ に __最善手__ が __複数存在__ した場合は、__それらの中__ から __同じ確率__ で __平等__ に、__ランダム__ に __最善手__ を __選択__ していました。しかし、__複数__ の __最善手__ が __存在__ した場合に、その中で __優劣__ は __存在しない__ と __考えて良いでしょうか__？__最強の AI__ を __作成__ するという __目的だけ__ を __考える__ のであれば、__最善手__ の間に __優劣__ は __ありません__ が、__別の観点__ から __見ると__、__最善手__ の __間__ で __優劣__ が __存在__ する場合が __あります__。本記事では、__必勝の局面__ と __必敗の局面__ の __2 つの場合__[^1] での、__複数の最善手__ の間の __違い__ について説明します。

なお、ここでいう __最善手__ とは、__演繹法__ による __条件__ によって、__最善手__ であることが __保証されている__ ものの事です。__ヒューリスティックスな条件__ によって __求められた__、__評価値__ が __最も高い合法手__ の事 __ではない__ 点に __注意__ して下さい。

## 必勝の局面での最善手の差異

__ルール 12 改__ の __条件__ の中で、「__勝てる場合__ に __勝つ__」と、『「__自 2 敵 0 空 1__」が __2 つ以上存在__ する着手を行う』という __条件__ は、__それら__ を満たす __合法手を選択__ することで、__必ず勝利できる__ ので、いずれも __必勝の局面__ で __最善手__ を __選択__ するという __条件__ です。__`ai12s` ver 2__ では、それらに対する __評価値__ を、先程示した表のように、__300__、__200__ という、__異なる値__ を __設定__ していたので、__それらの条件__ を満たす __合法手__ が __どちらも存在__ する場合は、__評価値__ が __高い__ 「__勝てる場合__ に __勝つ__」ほうの __合法手__ が __選択される__ ことになります。

しかし、__どちらの条件__ による __合法手__ も __最善手である__ ことに __変わりはない__ ので、__同じ評価値__ を __設定__ しても、__弱解決の AI__ であることは __変わりません__。__そのことを示す__ ために、その __2 つの条件__ の __評価値__ を __同じ値__ にして __ランダムな AI__ と __対戦__ してみることにします。

### `ai12s` の修正

上記の 2 つの __評価値__ を __修正__ する際に、毎回 __`ai12s`__ の __プログラム__ を __変更__ するのは __大変__ なので、「__勝てる場合__ に __勝つ__」と、『「__自 2 敵 0 空 1__」が __2 つ以上存在__ する着手を行う』に対する __評価値__ も、__パラメータ__ として __実引数__ で __設定できる__ ように __`ai12s`__ を __修正する__ ことにします。また、せっかくなので、__他の評価値__ も __実引数__ で __設定__ できるようにします。

それぞれの __パラメータを代入__ する __仮引数__ の名前は、__下記__ のように __命名__ しました。

| 局面の状況 | 仮引数の名前 | 仮引数の名前の由来 |
|:--|:--|:--|
| __自分が勝利している__  | `score_victory` | 勝利を表す victory |
| __「自 2 敵 0 空 1」が<br> 2 つ以上存在する__  | `score_sure_victory` | 必勝を表す sure victory |
| __片方__ の __斜め方向__ に __〇×〇__ が<br>__並び__、__いずれか__ の __辺__ の __1 つの<br>マスのみ__ に __× が配置__ されている  | `score_special` | 特殊を表す special |
| __相手が勝利できる__  | `score_defeat` | 敗北を表す defeat |

下記は、上記の __仮引数__ を、__これまでのパラメータ__ を __デフォルト値__ として持つ __デフォルト引数__ として __追加__ した __`ai12s`__ のプログラムです。

* __1、2 行目__：上記の __仮引数__ を __デフォルト引数__ として追加する
* __7、14、17、28__：__返り値__ の __値__ を __仮引数__ に __修正__ する

```python
 1  def ai12s(mb, score_victory=300, score_sure_victory=200, score_defeat=-100,
 2            score_special=100, score_201=2, score_102=0.5, score_012=-1, debug=False):  3  
 4      def eval_func(mb):         
 5          # 自分が勝利している場合
 6          if mb.status == mb.last_turn:
 7              return score_victory
 8
 9          markpats = mb.count_markpats()
10          if debug:
11              pprint(markpats)
12          # 相手が勝利できる場合
13          if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
14              return score_defeat
15          # 次の自分の手番で自分が必ず勝利できる場合
16          elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
17              return score_sure_victory
18        
19          # 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
20          if mb.board[1][1] == Marubatsu.CROSS and \
21             (mb.board[0][0] == mb.board[2][2] == Marubatsu.CIRCLE or \
22              mb.board[2][0] == mb.board[0][2] == Marubatsu.CIRCLE) and \
23             (mb.board[1][0] == Marubatsu.CROSS or \
24              mb.board[0][1] == Marubatsu.CROSS or \
25              mb.board[2][1] == Marubatsu.CROSS or \
26              mb.board[1][2] == Marubatsu.CROSS) and \
27             mb.move_count == 4:
28              return score_special       
以下同じなので略
```

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

```python
def ai12s(mb, score_victory=300, score_sure_victory=200, score_defeat=-100,
          score_special=100, score_201=2, score_102=0.5, score_012=-1, debug=False):    
    def eval_func(mb):         
        # 自分が勝利している場合
        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

    return ai_by_score(mb, eval_func, debug=debug)
```
</details>

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

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

        markpats = mb.count_markpats()
        if debug:
            pprint(markpats)
        # 相手が勝利できる場合
        if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
-           return -100
+           return score_defeat
        # 次の自分の手番で自分が必ず勝利できる場合
        elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
-           return 200
+           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 -100
+           return score_special    
以下同じなので略
```
</details>

### ランダムな AI との対戦

__下記__ のプログラムで、「__自分が勝利__ している場合」の __評価値__ を、『「__自 2 敵 0 空 1__」が __2 つ以上存在__ する場合』の __評価値__ と __同じ 200__ に __設定__ して、__ランダムな AI__ である __`ai2` と対戦__ してみることにします。以後は、__このパラメータ__ の __AI__ を __`ai12s` ver 3__ と __表記__ します。

```python
ai_match(ai=[ai12s, ai2], params=[{ "score_victory": 200 }, {}])
```

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

```
ai12s VS ai2
count     win    lose    draw
o        9577       0     423
x        8686       0    1314
total   18263       0    1737

ratio     win    lose    draw
o       95.8%    0.0%    4.2%
x       86.9%    0.0%   13.1%
total   91.3%    0.0%    8.7%
```

__実行結果__ から、__敗率が 0 %__ であることが __確認__ できたので、__`ai12s` ver 3__ は __高確率__ で __弱解決の AI__ であると考えることができます。

下記は、__これまで__ の __`ai12s`__ VS __`ai2`__ の __対戦結果__ を __加えた__ ものです。

| 関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 |
|:-:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| __`ai12s` ver 1__  | __98.9__ | __0.0__ | 1.1 | __88.2__ | __0.0__ | 11.8 | __93.5__ | __0.0__ | 6.5 |
| __`ai12s` ver 2__  | __98.9__ | __0.0__ | 1.1 | __88.7__ | __0.0__ | 11.3 | __93.8__ | __0.0__ | 6.2 |
| __`ai12s` ver 3__  | __95.8__ | __0.0__ | 4.2 | __86.9__ | __0.0__ | 13.1 | __91.3__ | __0.0__ | 8.7 |

__上記__ の表から、__`ai12s` ver 3__ では、それ __以前のバージョンより__ も __引き分け率__ が __高くなる__ ことがわかります。__`ai12s` ver 3__ と、それ __以前のバージョン__ の __違い__ は __異なる最善手__ を __選択__ する __場合がある__ 点ですが、__最善手__ を __選択__ することに __変わりはない__ ので、上記のように、__引き分け率__ が __多くなる__ のは __明らかに変__ です。

実は、__この現象__ は、__筆者__ にとっても __想定外の現象__ でした。__原因__ を __調べてみた__ 所、「__自分が勝利__ している場合」の __評価値__ を、『「__自 2 敵 0 空 1__」が __2 つ以上存在__ する場合』の __評価値__ を __同じ値にする__ ことによって、__思わぬバグが発生する__ ことが __わかりました__。

この __バグの原因__ を __調べる方法__ は、__プログラム__ の __デバッグ__ の __具体例__ として __参考になる__ のではないかと思いましたので、__最善手__ の __優劣__ の話は、__次回の記事__ で行うことにし、__今回の記事__ では、__予定を変更__ してこのようなことが起きる __原因を検証__ することにします。

# `ai12s` ver 3 VS `ai2` の検証

[以前の記事](https://qiita.com/ysgeso/items/5fe28ae6c765a169fd34#%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E3%81%AA-ai-%E3%81%AE%E6%80%A7%E8%B3%AA)で説明したように、__ランダムな AI__ は、__世の中__ に __存在__ する __すべての AI__ が __選択する着手__ と、__まったく同じ着手__ を __選択する可能性__ があります。そのため、__ランダムな AI__ と __何度も対戦する__ ことで、__理論上__ は __すべての AI__ と __1 回以上対戦__ することが __できます__。

従って、__ランダムな AI__ である __`ai2`__ と __何度も対戦__ した際に、__`ai12s` ver 3__ のほうが、__`ai12s` ver 2 より__ も __引き分け率__ が __増えた__ ことは、__`ai12s` ver 2__ が __対戦__ すると __勝利__ するが、__`ai12s` ver 3__ が __対戦__ すると __引き分け__ になる __AI__ が __いくつか存在する__ ことを __意味__ します。

__そのような AI__ を __作る__ ことが __できれば__、__その AI__ と __`ai12s` ver 2__ や __`ai12s` ver 3__ との __対戦経過__ の __違い__ を __検証__ することで、__引き分け__ が __増えた原因__ を __調べる__ ことが __できます__。

__そのような AI__ は、__下記の手順__ で __見つける__ ことが __できます__。

1. __世の中に存在__ する __AI の中__ から __ランダム__ に __AI__  を __一つ選択__ する
2. __`ai12s` ver 2__ VS __その AI__ の __対戦__ を行い __結果を記録__ する
3. __`ai12s` ver 3__ VS __その AI__ の __対戦__ を行い __結果を記録__ する
4. __手順 2__ と __3__ の __対戦結果__ が __同じ場合__ は __手順 1__ に __戻る__
5. __対戦結果__ が __異なる場合__ は、__その AI__ が __求める AI__ である

__ランダムな AI__ と __対戦する__ ということは、__世の中に存在__ する __AI の中__ から __ランダム__ に __AI__  を __一つ選択__ して __対戦する__ ことを __意味する__ ので、上記の __手順 1__、__2__  は __`ai2` と対戦__ することで、__簡単に行う__ ことが __できます__。しかし、__`ai2`__ は __対戦__ を __行うたび__ に、__ランダム__ に __AI__ を __選択し直す__ ので、残念ながら __`ai2` と対戦__ することで __手順 3__ を __行うこと__ は __できません__。

__この問題__ を __解決__ するためには、__プログラムの再現性__ について __理解__ する __必要__ が __あります__。

## ランダムな処理とプログラムの再現性

一般的に、__ランダムな処理__ を __行わない__ プログラムは、__行う処理__ に __関係する条件__ が __同じ状態__ でプログラムを __実行__ した場合は、__毎回同じ処理__ を __行います__。例えば、__下記__ の __`1 + 2`__ は __ランダムな処理__ を __行わない__ ので、__2 回実行__ しても、画面に __必ず 3 が表示__ されます。


```python
print(1 + 2)
print(1 + 2)
```

実行結果

```
3
3
```

下記の __`add`__ という __関数__ は、__ランダムな処理__ を __行わないの__ で、__下記__ のプログラムのように、__同じ実引数__ を __記述__ して __呼び出す__ と、__必ず同じ数値__ が __表示__ されます。

```python
def add(x, y):
    print(x + y)

add(1, 2)
add(1, 2)
```

実行結果

```
3
3
```

:::note warn
このような、__同じ条件__ で __実行__ すると、__必ず同じ処理__ が行われる __プログラム__ のことを、__再現性がある__ プログラムと __呼びます__。
:::

:::note
__ランダムな処理__ を __行わない__ プログラムであっても、例えば __時間を表示__ するプログラムのような __場合__ などでは、一見すると __条件__ が __同じように見える__ にも関わらず、__毎回異なる表示__ が __行われる__ 場合が __あります__。しかし、その場合は、__プログラム__ を __実行する時間__ という __条件が異なる__ ので、__異なる処理__ が __行われます__。

なお、__上記__ の __`add`__ という __関数__ は、__時間__ に __関する処理__ を __行わない__ ので、__実行する時間__ が __異なっても__、毎回 __同じ処理__ が __行われます__。
:::

一方、__`ai2`__ のように、__ランダムな処理__ を __行う__ プログラムは、__下記__ のプログラムのように、実行するたびに __異なる処理__ が __行われます__。

```python
mb = Marubatsu()
print(ai2(mb))
print(ai2(mb))
```

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

```
(1, 1)
(1, 2)
```

:::note warn
このような __同じ条件__ で __実行__ しても __毎回異なる処理__ を行う __プログラム__ の事を、__再現性のない__ プログラムと __呼びます__。
:::

なお、__プログラム__ では __乱数__ を使って __ランダムな処理__ を __行う__ ので、以後は __ランダムな処理を行うプログラム__ の事を、__乱数を使ったプログラム__ と __表記__ することにします。

## プログラムの再現性の重要性

__バグが発生__ した場合は、その __バグの原因__ を __検証__ して __取り除く必要__ があります。__バグ__ を __修正__ するには __様々な方法__ が __あります__ が、__プログラム__ を __見ても__ 原因が __良くわからない場合__ は、[前回の記事](https://qiita.com/ysgeso/items/70c50f75319373399871#ai2-vs-ai11s-%E3%81%AE%E6%A4%9C%E8%A8%BC)で __`ai11s`__ の __バグの原因__ を __調べる__ するために __`ai2`__ VS __`ai11s`__ の __検証__ を行ったように、__バグが発生__ する __状況__ を __再現__ して __検証する__ という __方法__ が __良く使われます__。

その際に、__再現性のある__ プログラム __であれば__、__バグが発生__ した場合と __同じ条件__ でその __プログラムを実行__ すると __同じバグ__ が __発生__ するので、__バグの原因__ を __検証__ することが __できます__。

一方、__乱数__ を __使ったプログラム__ には、一般的に __再現性がない__ ため、__バグが発生__ した __場合__ に、その __バグの原因__ を __検証__ することが __難しい__ という __問題__ があります。その __理由__ は、__特定の乱数__ が __発生した場合だけ__ で __エラーが発生する__ ような __バグ__ の場合は、__もう一度__ その __プログラム__ を __実行__ しても、その __エラーが発生しない可能性が高い__ からです。

例えば、__100 万分の 1__ の __確率__ で __バグが発生する__ ような __プログラム__ があった場合に、__バグの原因__ を __検証__ しようと思っても、再び __同じバグが発生__ するまで、そのプログラムを  __約 100 万回__ も __実行する必要__ が __あります__。

## 乱数の種を使った、再現性のある乱数を利用するプログラム

__乱数を利用__ するプログラムにも __再現性__ を __持たせる__ ことが __できれば__、__上記の問題__ を __解決__ することが __できます__。また、先ほどの __手順 2__ と __3__ で、__`ai12s` ver 2__ と __`ai12s` ver 3__ が、__手順 1__ で __ランダムに選択__ した __同一の AI__ と __対戦__ することが __できるよう__ に __なります__。

__乱数__ と __再現性__ は __矛盾__ する要素だと __思う人__ が __いるかもしれません__ が、ここでいう __再現性とは__、__一度発生__ させた __複数の乱数__ と、__全く同じパターン__ の __乱数__ を __何度でも発生__ させて __利用する__ ことが __できる__ という __意味__ での __再現性__ です。

例えば、__サイコロ__ を __10 回__ 振った際の __出目__ を __記録__ しておき、__次__ に __サイコロ__ を __振る必要__ が __生じた際__ に、__過去__ に __振ったサイコロ__ の __出目__ を __再利用__ するということに __相当__ します。

このような __乱数__ の __再現性__ は、__乱数の種__ を __利用__ することで __実現__ することが __できます__。__乱数の種__ の __意味__ と __使い方__ を __理解__ するために、__乱数の仕組み__ について __説明__ します。

## 乱数の仕組み

__コンピューター__ が __発生__ させる __乱数__ は、実は __本当の意味__ での __乱数ではなく__、__計算__ によって __求められた__、乱数のように見える __疑似乱数__ と呼ばれるものです。それに対して、__本当の意味__ での __でたらめな数__ のことを、__真の乱数__ と呼びます。残念ながら、__数学__ の __計算__ は、__同じデータ__ に対して __同じ計算__ を行うと、__同じ計算結果__ が __得られる__ ため、__コンピュータ__ は __計算__ によって __真の乱数__ を __発生__ させることは __できません__。

:::note
__真の乱数__ を __発生__ させるためには、__特別な装置__（ハードウェア） が __必要__ ですが、__一般的なコンピュータ__ には __そのような装置__ は __備わっていません__。
:::

__疑似乱数__ の __最も簡単__ な __作り方__ の __一つ__ に、__下記__ の __手順__ で __乱数を計算__ する、__線形合同法__ という __アルゴリズム__ があります。興味がある方は、下記に線形合同法のリンクを紹介しますので、そちらを見て下さい。

1. __整数__ $A$、$B$、$M$ を __何らかの値__ で __定める__。ただし、$M > A$、$M > B$、$A > 0$、$B >= 0$ で、 $A$、$B$、$M$ は __定数__[^2]である
2. __乱数の種__ と呼ばれる __整数__ $X_0$ を __決める__。これは、__任意の値__ を __設定__ しても __良い__
3. __以下__ の __計算式__ で __乱数__ $X_n$ を __計算__ する。__mod__ は __余り__ を __計算する__ という __演算子__ である

    $X_{n+1} = (A × X_n + B)　mod　M$

__具体例__ を挙げます。

$A = 5、B = 0、M = 7、X_0 = 1$ とした場合、__下記__ の __手順__ で、__5__、__4__、__6__、__2__、__3__、__1__、__5__、__4__、__6__、__2__、__3__、__1__、・・・ という __乱数__ を __次々に計算__ することが __できます__。

$X_1 = 5 × 1　mod　7 = 5　mod　7 = (0 × 7 + 5)　mod　7 = 5$
$X_2 = 5 × 5　mod　7 = 25　mod　7 = (3 × 7 + 4)　mod　7 = 4$
$X_3 = 5 × 4　mod　7 = 20　mod　7 = (2 × 7 + 6)　mod　7 = 6$
$X_4 = 5 × 6　mod　7 = 30　mod　7 = (4 × 7 + 2)　mod　7 = 2$
$X_5 = 5 × 2　mod　7 = 10　mod　7 = (1 × 7 + 3)　mod　7 = 3$
$X_6 = 5 × 3　mod　7 = 15　mod　7 = (2 × 7 + 1)　mod　7 = 1$
$X_7 = 5 × 1　mod　7 = 5　mod　7 = (0 × 7 + 5)　mod　7 = 5$
・・・以下 __同様__ の __計算__ が __繰り返される__

下記は、__上記の乱数__ を __計算__ する __プログラム__ です。__実行結果__ から、__上記と同じ乱数__ が __計算__ できていることが __わかります__。

```python
A = 5
B = 0
M = 7
X = 1
for _ in range(10):
    X = A * X % M
    print(X, end=",")
```    

実行結果

```
5,4,6,2,3,1,5,4,6,2,
```


__この例__ から __わかるよう__ に、__線形合同法__ によって __計算__ された __疑似乱数__ には、__同じパターン__ の __乱数__ が __繰り返される__ という __周期性__ が __あります__。ただし、$M$ を __大きな値__ にすることで、__周期性__ があることが __わかりづらい__、__長い周期__ の __疑似乱数__ を __計算できます__。

__線形合同法__ は、__計算__ が __簡単__ なため __計算時間__ が __短く__、__コンピューターの性能__ が今のコンピューターと比較して __何万分の一以下__ だった __2000 年以前__ の頃は __良く使われていました__ が、__疑似乱数__ の __品質が悪い__、__偏った乱数__ が __作られてしまう__ という __欠点__ があるので、__最近__ では __あまり使われません__。Python の __random モジュール__ など、__最近__ の __プログラム言語__ では __もっと複雑な計算式__ で、__品質の良い疑似乱数__ を __計算__ する __関数__ が __用意されています__。

参考までに、乱数に関する Wikipedia のリンクを紹介します。

https://ja.wikipedia.org/wiki/%E4%B9%B1%E6%95%B0%E5%88%97

https://ja.wikipedia.org/wiki/%E6%93%AC%E4%BC%BC%E4%B9%B1%E6%95%B0

https://ja.wikipedia.org/wiki/%E7%B7%9A%E5%BD%A2%E5%90%88%E5%90%8C%E6%B3%95

## 乱数の種

__線形合同法__ の $X_0$ のように、__乱数__ を __計算__ する際に、__最初__ に __任意の数値__ として __設定できる値__ のことを、__乱数の種__ と呼び、__どのような疑似乱数__ の __アルゴリズム__ であっても、__疑似乱数__ を __計算し始める際__ に、__何らかの値__ を __設定する必要__ が __あります__。__疑似乱数__ は、__乱数の種__ を __元__ に __計算__ して __作られる__ ので、__同じ乱数の種__ から __計算__ された __疑似乱数__ は、毎回 __同じパターン__ の __疑似乱数__ を __発生__ させます。つまり、__乱数の種__ を使うことで、__乱数__ を __使った__ プログラムに __再現性__ を __持たせる__ ことが __できる__ ようになります。

### 乱数の種の設定方法

Python の __random モジュール__ には、__乱数の種__ を __設定__ する __`seed`__ という __関数__ があり、この関数の __実引数__ に __乱数の種__ を表す __整数__ を記述して呼び出すことで、__乱数の種__ を __設定__ することが __できます__。また、__同じ乱数の種__ を __設定__ することで、__その後__ で __発生__ する __乱数__ が __毎回同じパターン__ になるという、__乱数__ の __再現性__ を __実現__ することが __できます__。

__具体例__ を挙げます。__random モジュール__ には、__指定した範囲__ の __整数__ の __乱数__ を __発生__ させる __`randint`__ という __関数__ が __定義__ されており、例えば __`randint(1, 6)`__ を __実行__ すると、__1 から 6__ までの __ランダムな整数__ を、__サイコロの__ ように __発生__ させることが __できます__。

__下記__ は、__1 ~ 6__ までの __整数の乱数__ を __10 回__ 計算して __表示する__ という __処理__ を、__乱数の種__ を __設定せず__ に __3 回__、__その後__ で __乱数の種__ に __0__ を __設定__ した後で、__3 回__ 行うプログラムです。

__実行結果__ から、__乱数の種__ を __設定しない場合__ は __毎回異なる__ 10 個の __疑似乱数__ が __表示__ されますが、__乱数の種__ を __0__ に __設定した後__ で __表示__ される __疑似乱数__ は、__3 回__ とも __全く同じパターン__ になることが __確認__ できます。

```python
import random

for i in range(3):
    for j in range(10):
        print(random.randint(1, 6), end=",")
    print()

for i in range(3):
    random.seed(0)
    for j in range(10):
        print(random.randint(1, 6), end=",")
    print()
```

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

```
5,4,2,2,4,1,5,6,5,2,
4,3,5,1,6,5,6,5,5,3,
1,1,2,3,2,3,2,4,2,4,
4,4,1,3,5,4,4,3,4,3,
4,4,1,3,5,4,4,3,4,3,
4,4,1,3,5,4,4,3,4,3,
```

:::note
__上記__ のプログラムで、__これまで__ のプログラムと __同様__ に、__`seed`__ と __`randint`__ を下記のプログラムのように __個別__ に __インポートしない理由__ は、__この後__ のプログラムで、__`seed`__ という __名前__ の __変数__ を __利用するため__ です。

```python 
from random import seed, randint
```
:::

`seed` と `randint` の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/random.html#random.seed

https://docs.python.org/ja/3/library/random.html#random.randint

### 乱数の種を設定しない場合に行われる処理

__これまで__ のプログラムでは、__乱数の種__ を __設定せず__ に、__乱数を発生__ させてきましたが、それにも関わらず、__ランダムな AI__ である __`ai2`__ が __毎回異なる合法手__ を __選択__ するのは __おかしい__ と __思った人__ は __いないでしょうか__？

__乱数の種__ を __設定しない__ 場合は、__プログラム__ を __実行した時間__ を __整数__ に __変換した値__ を使って、__random モジュール__ を __インポート__ した __時点__ で __乱数の種__ が __自動的に設定__ されます。そのため、__異なる時間__ で __実行した場合__ は、__異なる乱数の種__ が __設定される__ ため、__プログラム__ を __実行するたび__ に __毎回異なるパターン__ の __乱数が発生する__ という __仕組み__ になっています。

従って、__乱数__ の __計算__ に __再現性__ を __求めない場合__ は、__乱数の種__ を __設定__ しては __いけません__。

:::note
上記からわかるように、__乱数__ を __利用するプログラム__ は、__プログラム__ を __実行した時間__  が __全く同じ__ であれば、__同じパターン__ の __乱数を発生__ するので、__再現性のある__ プログラムです。ただし、__全く同じ時間__ に __プログラムを実行__ することは __現実的__ にはほぼ __不可能__ なので、__実質的__ には __再現性のない__ プログラムと __考えて良い__ でしょう。
:::



## 乱数の種を使った AI どうしの対戦の再現

__下記__ のプログラムは、__`ai12s` ver 2__ VS __`ai2`__ の __対戦__ を、__乱数の種__ を __設定せず__ に __3 回__ 行い、その後で __乱数の種__ に __0 を設定__ して __3 回行う__ プログラムです。

```python
mb = Marubatsu()

for i in range(3):
    mb.play(ai=[ai2, ai2])

for i in range(3):
    random.seed(0)
    mb.play(ai=[ai2, ai2])
```

<details><summary><strong>実行結果（長いのでクリックして開いてください）</strong></summary>

```
Turn o
...
...
...

Turn x
...
O..
...

Turn o
..X
o..
...

Turn x
..x
oO.
...

Turn o
.Xx
oo.
...

Turn x
Oxx
oo.
...

Turn o
oxx
oo.
.X.

winner o
oxx
oo.
.xO

Turn o
...
...
...

Turn x
..O
...
...

Turn o
..o
..X
...

Turn x
O.o
..x
...

Turn o
o.o
..x
..X

winner o
oOo
..x
..x

Turn o
...
...
...

Turn x
...
..O
...

Turn o
...
..o
..X

Turn x
...
.Oo
..x

Turn o
X..
.oo
..x

winner o
x..
Ooo
..x

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo
```
</details>

__実行結果__ から、__最初__ の __3 回__ の __対戦__ の __試合経過__ は __毎回異なります__ が、その後の __3 回__ の __対戦__ は、__全く同じ試合経過__ で __対戦__ が行われることが __確認__ できます。

従って、__乱数の種__ によって __ランダムな AI__ の __対戦__ を __再現できる__ ことが __確認__ できました。


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

__ランダムな AI__ の __対戦__ を __再現する際__ に、__`random.seed`__ を __呼び出してから__、__`play`__ メソッドを __呼び出す__ のは __面倒__ なので、__`play`__ メソッドの __仮引数__ に __乱数の種__ を __代入__ するように __修正__ し、実際の __乱数の種__ の __設定__ は、__`play`__ メソッドの __中で行う__ ようにします。

__そのようにする__ 場合は、__仮引数__ を __下記__ のように __設定__ するのが __一般的__ です。__仮引数__ を __デフォルト引数__ にすることで、その __仮引数__ に __対応する実引数__ を __記述するか__ どうかで、__関数__ が __行う処理__ の __再現性__ の __有無__ を __選択__ することが __できる__ ように __なります__。

* __仮引数__ の __名前__ を __種__（seed）を表す __`seed`__ とする
* __仮引数__ を、__デフォルト値__ が __`None`__ の __デフォルト引数__ にする
* __仮引数__ に __代入__ された __値__ が __`None` 以外__ の場合に、__その値__ を使って __乱数の種__ を __設定__ し、__`None` の場合__ は __何もしない__

下記は、__乱数の種__ を __設定できる__ ように、__`play`__ メソッドを __修正__ したプログラムです。

* __1 行目__：__デフォルト値__ に __`None`__ を __設定__ した __デフォルト引数 `seed`__ を __追加__ する
* __3、4 行目__：__`seed`__ が __`None` でない__ 場合は、__`seed`__ を __乱数の種__ として __設定__ する

```python
1  def play(self, ai, params=[{}, {}], verbose=True, seed=None):
2      # seed が None でない場合は、seed を乱数の種として設定する
3      if seed is not None:
4          random.seed(seed)
元と同じなので略
5
6  Marubatsu.play = play
```

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

```python
def play(self, ai, params=[{}, {}], verbose=True, seed=None):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)

    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        if verbose:
            print(self)
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self, **params[index])
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        print(self)
    return self.status

Marubatsu.play = play
```
</details>

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

```diff_python
def play(self, ai, params=[{}, {}], verbose=True, seed=None):
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
元と同じなので略

Marubatsu.play = play
```
</details>


__下記__ のプログラムは、__先程と同様__ に __`ai12s` ver 2__ VS __`ai2`__ の __対戦__ を、__乱数の種__ を __設定せず__ に __1 回__ 行い、__その後__ で __乱数の種__ に __0 を設定__ して __2 回行う__ プログラムです。

```python
mb = Marubatsu()

for i in range(3):
    mb.play(ai=[ai2, ai2])

for i in range(3):
    mb.play(ai=[ai2, ai2], seed=0)
```

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

```diff_python
mb = Marubatsu()

for i in range(3):
    mb.play(ai=[ai2, ai2])

for i in range(3):
-   random.seed(0)
-   mb.play(ai=[ai2, ai2])
+   mb.play(ai=[ai2, ai2], seed=0)

```
</details>


<details><summary><strong>実行結果（長いのでクリックして開いてください）</strong></summary>

```
Turn o
...
...
...

Turn x
...
...
.O.

Turn o
...
..X
.o.

Turn x
...
.Ox
.o.

Turn o
.X.
.ox
.o.

Turn x
.x.
.ox
.oO

Turn o
.xX
.ox
.oo

Turn x
.xx
Oox
.oo

winner x
Xxx
oox
.oo

Turn o
...
...
...

Turn x
.O.
...
...

Turn o
.o.
..X
...

Turn x
.o.
..x
O..

Turn o
.o.
..x
o.X

Turn x
.o.
..x
oOx

winner x
.oX
..x
oox

Turn o
...
...
...

Turn x
...
.O.
...

Turn o
.X.
.o.
...

Turn x
.x.
.o.
.O.

Turn o
Xx.
.o.
.o.

Turn x
xx.
.oO
.o.

Turn o
xx.
.oo
.oX

Turn x
xx.
.oo
Oox

winner x
xxX
.oo
oox

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo

Turn o
...
...
...

Turn x
...
...
O..

Turn o
...
...
oX.

Turn x
O..
...
ox.

Turn o
o..
X..
ox.

Turn x
o..
x..
oxO

Turn o
o..
x.X
oxo

Turn x
o.O
x.x
oxo

winner x
o.o
xXx
oxo
```
</details>

__先程と同様__ に、__最初__ の __3 回__ の __試合経過__ は __毎回異なります__ が、__その後__ の __3 回__ の __試合経過__ は __毎回必ず同じに__ なることが __確認__ できます。

## `ai12s` ver 2 VS `ai2` と `ai12s` ver 3 VS `ai2` の違いの表示

先程紹介した、__ランダムな AI__ である __`ai2` と対戦__ した際に、__`ai12s` ver 2__ と __対戦__ すると __勝利__ するが、__`ai12s` ver 3__ と __対戦__ すると __引き分け__ になる __AI__ を見つける __下記__ の __アルゴリズム__ は、__乱数の種__ を使って __記述__ することができます。どのようなプログラムを記述すれば良いかについて少し考えてみて下さい。

1. __世の中に存在__ する __AI の中__ から __ランダム__ に __AI__  を __一つ選択__ する
2. __`ai12s` ver 2__ VS __その AI__ の __対戦__ を行い __結果を記録__ する
3. __`ai12s` ver 3__ VS __その AI__ の __対戦__ を行い __結果を記録__ する
4. __手順 2__ と __3__ の __対戦結果__ が __同じ場合__ は __手順 1__ に __戻る__
5. __対戦結果__ が __異なる場合__ は、__その AI__ が __求める AI__ である

上記の __手順 1__ ~ __3__ は、__下記__ のような方法で __処理を行う__ ことができます。

| 手順 | 処理の方法 |
|:--|:--|
| __手順 1__  | __これまで__ に __選択していない__、__乱数の種__ を __1 つ選択する__   |
| __手順 2、3__  | __対戦__ を __行う前__ に、__手順 1__ で __選択__ した __乱数の種__ を __設定する__  |

__手順 1__ で __選択__ する __最初__ の __乱数の種__ は __何でも構わない__ ので、__本記事__ では __0 を選択__ することにします。また、__手順 1__ で __その後__ に __選択__ する __乱数の種__ には、__直前__ に __選択__ した __乱数の種__ に __1 を足した値__ を __選択__ するという方法が __最も簡単__ でしょう。__1 を足す__ ことで、これまでに選択した __どの乱数の種より__ も __大きな値になる__ ので、__これまでに選択__ した __乱数の種__ とは、__必ず異なる値__ を __選択__ することが __できます__。

下記は __上記の処理__ を行うプログラムです。

* __1 行目__：__乱数の種__ を __代入__ する変数 __`seed`__ を __0 で初期化__ する（__手順 1__）
* __3 ~ 11 行目__：__答え__ が __見つかる__ まで __無限に繰り返す__
* __4 行目__：__`seed`__ に __代入__ された __乱数の種__ で 、__`ai12s` ver 2__ VS __`ai2`__ の __対戦__ を __対戦経過__ を __表示せず__ に（`verbose=False`）行い、__結果__ を __`winner1`__ に __代入__ する（__手順 2__）
* __5、6 行目__：__`seed`__ に __代入__ された __乱数の種__ で 、__`ai12s` ver 3__ VS __`ai2`__ の __対戦__ を __対戦経過__ を __表示せず__ に（`verbose=False`）行い、__結果__ を __`winner2`__ に __代入__ する（__手順 3__）
* __7 ~ 10 行目__：それぞれの __対戦結果__ が __異なる場合__ に、__その乱数の種__ で __`ai12s` ver 2__ VS __`ai2`__ と __`ai12s` ver 3__ VS __`ai2`__ の __対戦経過__ を __表示__ し、__無限ループ__ から __抜ける__（__手順 5__）
* __11 行目__：__乱数の種__ に __1 を足す__（__手順 1__）。ここで __while 文__ の __ブロックが終了__ しているので、__4 行目__ に __戻り__、__次の繰り返し処理__ が __行われる__（__手順 4__）

```python
 1  seed = 0
 2  mb = Marubatsu()
 3  while True:
 4      winner1 = mb.play(ai=[ai12s, ai2], seed=seed, verbose=False)
 5      winner2 = mb.play(ai=[ai12s, ai2], params=[{"score_victory": 200}, {}], \
 6                        seed=seed, verbose=False)
 7      if winner1 != winner2:
 8          mb.play(ai=[ai12s, ai2], seed=seed)
 9          mb.play(ai=[ai12s, ai2], params=[{"score_victory": 200}, {}], seed=seed)
10          break
11      seed += 1
```

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

```python
seed = 0
mb = Marubatsu()
while True:
    winner1 = mb.play(ai=[ai12s, ai2], seed=seed, verbose=False)
    winner2 = mb.play(ai=[ai12s, ai2], params=[{"score_victory": 200}, {}], \
                      seed=seed, verbose=False)
    if winner1 != winner2:
        mb.play(ai=[ai12s, ai2], seed=seed)
        mb.play(ai=[ai12s, ai2], params=[{"score_victory": 200}, {}], seed=seed)
        break
    seed += 1
```
</details>

実行結果
```
Turn o
...
...
...

Turn x
...
.O.
...

Turn o
...
.oX
...

Turn x
..O
.ox
...

Turn o
..o
.ox
..X

winner o
..o
.ox
O.x

Turn o
...
...
...

Turn x
...
.O.
...

Turn o
...
.oX
...

Turn x
..O
.ox
...

Turn o
..o
.ox
..X

Turn x
..o
.ox
.Ox

Turn o
..o
.ox
Xox

Turn x
O.o
.ox
xox

Turn o
oXo
.ox
xox

winner draw
oxo
Oox
xox
```

__下図__ は、__上記__ の __プログラム__ で __発見__ した、__結果__ が __異なる `ai12s` ver 2__ VS __`ai2`__ と __`ai12s` ver 3__ VS __`ai2`__ の __試合経過__ を __上下に並べた__ ものです。__図__ から、__4 手目まで__ の __試合経過__ は __同じ__ であり、__5 手目__ で __`ai11s` ver 2__ と __ver 3__ が __選択__ する __合法手__ が __異なったため__、__試合の結果__ が __異なる__ ように __なった__ ことが __わかりました__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/d2efa1de-3747-5554-2594-498474f6cf9c.png" width="700">

従って、__5 手目__ で __`ai12s` ver 3__ が __行う着手__ を __検証すればよい__ ことが __わかりました__。

記事が長くなったので、その検証は次回の記事で行うことにします。

# 今回の記事のまとめ

今回の記事では、__ルール__ の __条件の統合__ について __説明__ し、実際に __ルール 12__ の __条件の 1 つ__ を __統合__ することが __できる__ ことを __示しました__。

また、__必勝の局面__ での __最善手__ を __見つける__ ための、__異なる条件__ の __評価値__ を __同じ__ にした __`ai12` ver 3__ で __`ai2`__ と __対戦__ を行った結果、__バグが発生__ しました。その __バグの原因__ を __見つけるため__ に、__プログラム__ の __再現性__ と、__乱数の種__ について __説明__ し、__問題__ が __発生__ した __可能性__ が __高い局面__ を __見つけました__。

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

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

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

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

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

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

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

# 次回の記事

https://qiita.com/ysgeso/items/6b78ddb53b254a850457

[^1]:本記事では説明は省略しますが、__引き分けの局面__ でも __同様__ です
[^2]: __一度決めた後__ は、__常に同じ値__ を __使い続ける値__ の事です