Pythonでドミニオン戦略解析 〜鍛冶屋ステロ編〜

#自己紹介
はじめまして、NCT48といいます。
ボードゲーム好きの変態(他称)です。
理系(非情報系)を卒業して3年半、だらだらと働いています。
普段のお仕事は組み込み系(C言語)です。

#この記事について
今まで「情報のアウトプット」を行なった事がありませんでした。(自堕落マン)
そろそろ何か1つくらい書いてみようかと思っているまま時は過ぎ...(半年)
で、ついに重い腰を上げ一番好きなボードゲーム「ドミニオン」で書いてみる事にします。
Pythonの練習も兼ねてやってみました。

__2018/11/08追記__
__改めて見返してあまりにもボロボロだったので、その2として色々書き直しました。__

__[Pythonでドミニオン戦略解析 〜鍛冶屋ステロ編 その2〜][1]__
[1]:https://qiita.com/NCT48/items/c2b6dce2a23074f137fe


#ドミニオンって？
>ドミニオン (Dominion) は、アメリカのボードゲーム（カードゲーム）。作者はドナルド・X・ヴァッカリーノ。2008年秋にアメリカのリオグランデゲームズ（英語版）社より発売され、日本では2009年にホビージャパンが完全日本語版を発売している。(wikipedia ドミニオンより)

詳しく説明すると長くなるので、解説サイトやレビューサイトをご確認下さい。
~~個人的に最高のボードゲームだと思っています。さあ、みんなでレッツドミニオン!~~

#本題...の前に
タイトルにある通り「鍛冶屋ステロ」に関して...の前に、最低限のルールだけ説明しようかなと。
~~そもそもドミニオン知らない人がこの記事を見に来てくれるのか?~~
・カードを買ってデッキを強くするゲーム
・最初のデッキは「銅貨7枚(1金)」と「屋敷3枚(1点)」から
・毎ターンの終了時、手札を全て捨て5枚補充する
・デッキが無くなったら、捨て札をシャッフルしデッキにする。
・アクションカードや財宝カードを買ってデッキを強化していく
・ゲーム終了時に勝利点が多い人が勝ち
・一般的に「属州4枚(6点*4枚=24点)」を集めれば負けはないと言われている

簡易的な説明のため一部語弊や誤解を招く表現がありますが、だいたいこんな感じです。

#ステロイド、略してステロ
ドミニオンには様々な戦法があります。
その中の一つがステロイド(ステロ)と呼ばれるものです。
最低限(1〜2枚)のアクションカードと大量の財宝カードを買い、属州4枚の獲得を目指す。
今回は「鍛冶屋」を主体にした「鍛冶屋ステロ」を解析してみました。

#解析データ
解析対象のデータはcsvで、50000回一人回しを行なった結果です。
(自作のシミュレータを使用して、結果を出力しています。)
出力内容は
・ID
・属州4枚買うまでのターン(Turn)
・各ターンの手札内容(Hund)
・各ターン使用しなかった財宝(UselessMoney)
となっています。
今回の解析ではID,Turn,Hundを使用しました。
(UselessMoneyはうまく活用できなかったです...)

#戦略詳細
1.手札に8金以上あり、デッキに金貨が1枚以上ある→属州購入
2.手札に8金以上あり、デッキに金貨が1枚もない→金貨購入
3.手札に6金以上ある→金貨購入
4.手札に4金以上あり、デッキに鍛冶屋が1枚以上ある→銀貨購入
5.手札に4金以上あり、デッキに鍛冶屋が1枚もない→鍛冶屋購入
6.手札に3金以上ある→銀貨購入

実践ではもう少し色々考えるんですが、簡易化のためにこの戦術でいきます。

#環境
Windows10
Jupyter Notebook
Python3
NumPy
Pandas
Matplotlib
(自作シミュレータはC#です。)

#戦略解析
ここからはソースと出力結果を合わせて解説していきます。

```python:smith.ipynb
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
```

ここは特に言うことはないと思います。

---

```python:smith.ipynb
smith_data = pd.read_csv('./TestProgramSmith.csv', header=0, index_col=0)
smith_data.head()
```

|  | Turn | Hund | UselessMoney |
|--:|--:|:--|:--|
| 0 | 14 | 屋屋銅銅銅/屋銅銅銅銅/屋屋銅銅銅/屋銀銅銅銅銅銅鍛/屋屋銀銅銅/屋銀銅銅銅銅銅鍛/屋金銀銅... | 0/0/0/1/1/1/1/3/1/0/2/3/0/1 |
| 1 | 12 | 屋銅銅銅銅/屋屋銅銅銅/屋屋銀銅銅銅銅鍛/屋銅銅銅銅/屋屋金銀銅銅銅鍛/屋屋金銀銅銅銅鍛/屋... | 0/0/0/1/2/0/1/1/2/1/2/0 |
| 2 | 17 | 屋銅銅銅銅/屋屋銅銅銅/屋銅銅銅銅/屋屋銀銅銅銅銅鍛/屋銀銅銅銅/屋銀銀銅銅/屋屋銀銅銅銅銅... | 0/0/1/0/2/0/0/0/3/1/3/1/1/3/0/0/7 |
| 3 | 13 | 屋銅銅銅銅/屋屋銅銅銅/銀銅銅銅銅/屋屋屋金銅銅銅鍛/銀銅銅銅銅/金金銀銅銅/屋屋屋金銅銅銅... | 0/0/0/0/0/2/0/3/2/1/2/0/1 |
| 4 | 13 | 屋屋銅銅銅/屋銅銅銅銅/屋屋銀銅銅銅銅鍛/屋銅銅銅銅/屋屋金銀銅銅銅鍛/屋屋銅銅銅/金銀銅銅... | 0/0/0/1/2/0/0/1/1/0/0/1/1 |

ここでHundの詳細な説明を。
今回登場するカードは
・屋敷(1点,2円)
・銅貨(1円,0円)
・銀貨(2円,3円)
・金貨(3円,6円)
・鍛冶屋(カードを3枚引く,4円)
・属州(6点,8円)
です。
ここで(X,Y)の[X=カードの効果]、[Y=カードを買うのに必要なお金]です。
「屋屋銅銅銅」となっていた場合、手札に「屋敷2枚、銅貨3枚」があると言うことです。
「屋銀銅銅銅銅銅鍛」となっていた場合、鍛冶屋を発動した結果、手札に「屋敷1枚、銀貨1枚、銅貨5枚」があると言うことです。
ターン毎に/が入っているので、「屋屋銅銅銅/屋銅銅銅銅」となっていた場合、「1ターン目/2ターン目」となっています。

---

```python:smith.ipynb
smith_data34 = smith_data[smith_data["Hund"].str.startswith("屋屋銅銅銅/", "屋銅銅銅銅/")]
smith_data25 = smith_data[smith_data["Hund"].str.startswith("銅銅銅銅銅/", "屋屋屋銅銅/")]
```

最初のデッキは銅貨7枚と屋敷3枚で手札は5枚なので、
1.銅貨2枚-銅貨5枚
2.銅貨3枚-銅貨4枚
3.銅貨4枚-銅貨3枚
4.銅貨5枚-銅貨2枚
の4パターンです。
このうち、1-4と2-3は同じ結果なのでそれぞれのパターンにDatasetを分けました。

---

```python:smith.ipynb
smith_data34_des = smith_data34["Turn"].describe()
smith_data25_des = smith_data25["Turn"].describe()

smith_data25_des, smith_data34_des
```

(count    4149.000000
 mean       15.847674
 std         1.701418
 min        11.000000
 25%        15.000000
 50%        16.000000
 75%        17.000000
 max        23.000000
 Name: Turn, dtype: float64, 
 count    20974.000000
 mean        14.833031
 std          1.676777
 min          9.000000
 25%         14.000000
 50%         15.000000
 75%         16.000000
 max         23.000000
 Name: Turn, dtype: float64)

平均ターン、最速ターン共に3-4の入りの方が上です。
この理由としては、2-5の入りだとデッキ2週目に銀貨がいない(鍛冶屋を買っている)→デッキの金量が低いからではないかと思います。
(銀貨を買った方が良いのかは次回以降に試したいと思います。)

---

```python:smith.ipynb
plt.hist([smith_data25["Turn"],smith_data34["Turn"]],bins=10, range=(9.5,19.5), density=True, rwidth=0.5)
```

![Unknown.png](https://qiita-image-store.s3.amazonaws.com/0/311955/140f7c72-0b43-340a-18ab-7f8b46df5a7c.png)


青色が2-5,オレンジが3-4です。
やはり山のピークが左に寄っているのは3-4ですね。
グラフのスケールが違いすぎるので正規化しています。

---

```python:smith.ipynb
smith_data_sizumi = smith_data[smith_data["Hund"].str.find("鍛") == 31]
smith_data_sizumazu = smith_data[smith_data["Hund"].str.find("鍛") != 31]
```

次に「底沈み」と呼ばれる現象について解析したいと思います。
底沈みとは、「1〜2ターン目に買ったアクションカードを5ターン目に引く」ことを指します。
一般的に発生して欲しくない(デッキの進みが遅くなる)と言われています。
詳細はこれまた長くなるので「ドミニオン 底沈み」でググってください。
果たして本当に遅くなるのかを解析したいと思います。

---

```python:smith.ipynb
smith_data_sizumi_des = smith_data_sizumi["Turn"].describe()
smith_data_sizumazu_des = smith_data_sizumazu["Turn"].describe()

smith_data_sizumi_des, smith_data_sizumazu_des
```

(count    7715.000000
 mean       15.357874
 std         1.675631
 min        10.000000
 25%        14.000000
 50%        15.000000
 75%        16.000000
 max        22.000000
 Name: Turn, dtype: float64, 
count    42285.000000
 mean        14.937519
 std          1.722500
 min          9.000000
 25%         14.000000
 50%         15.000000
 75%         16.000000
 max         23.000000
 Name: Turn, dtype: float64)

約15%の確率で発生してます。
平均で0.4ターン、最速、最遅ターン共に1ターン遅くなっています。

---

```python:smith.ipynb
plt.hist([smith_data_sizumi["Turn"],smith_data_sizumazu["Turn"]],bins=10, range=(9.5,19.5), density=True, rwidth=0.5)
```

![Unknown-1.png](https://qiita-image-store.s3.amazonaws.com/0/311955/166930e5-d8b7-b50d-b8db-e65de1753dd1.png)


青色が沈んだ時,オレンジが沈まなかった時です。
沈んだ時はバラツキも大きく、確かに発生しない方が良いですね。
やはり、グラフのスケールが違いすぎるので正規化しています。

#まとめ
今まで体系的に言われきた「ドミニオンあるある」は本当なんだなぁと改めて思いました。
至極当たり前のことしか言ってない気もしますが、改めてデータで見るのも大事だなと言ったところです。
今後は「使い切れなかったお金(今回無駄にしたデータ)」や「アクションを発動した回数(シミュレータの改良が必要)」あたりを解析に生かしていきたいです。


#最後に
今回初めてPythonでデータ解析を行いました。
かなりツッコミどころが多いと思います。
コメント等にてガンガン突っ込んでもらえると嬉しいです。
もしドミニオンに詳しい人がいたら「こんな戦術試してくれ！」的なリクエストがあるともっと嬉しいです。~~コンボ系はやめてくださいしんでしまいます~~
(次回は「船着場ステロ」を予定してます。実はGitHubにはソースとか置いてあります。)

今回使用したソースおよびCSVです。
https://github.com/NCT48/Dominion