ブラックジャックを完璧にプレーイングするPythonプログラムを組んで、儲けることができるか検証した結果


タイトルに興味を示していただきありがとうございます。
初めての記事のため読みづらいところがあると思いますが、ぜひ最後まで読んでいただきますと、ブラックジャックのルールや完璧にプレーイングするPythonプログラムの内容について理解していただけると思います。ちなみにPythonを学び始めてあまりたっていないのでクラスのところでとんちんかんなことを言ってる可能性があるので、その場合は指摘をお願いします。

#２０万円すった話と結論
では早速結論から申し上げたいと思います。ブラックジャックにおけるチートシートであるベーシックストラテジー（後述）通りの完璧なプレーイングをしても最後には<b>必ず全持ち金が消え去ります</b>。（縦：所持金　横：プレー回数）
![kakezentai.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1138601/d2b9be95-6f71-50c2-bb38-0a5dcb907481.png)

ギャンブルで一攫千金なんて甘い話は本当に存在しないらしいです。しかし著者がこれに気づくまでには結構時間がかかりました。。。　
といいますのも実際にマカオやシンガポールのリアルなカジノを訪れて、毎回１０万円の計３０万円ほど利益を出していたからです。儲かっているのでは！と思われるかもしれませんが、調子に乗っていざ本場のラスベガスのカジノにて、よりハイベットで回していたところ３時間で５０万の全軍事金を溶かしてしまいました。
今でも放心状態でカジノを出て、あれだけ輝いていたネオン灯がくすんでしまった景色を思い出せます。またその後のご飯は全部サブウェイに格下げしたのも苦い思い出です。
その後、２年間ほどギャンブルからは手を引いて堅実な生活をしていましたが、先日ふとラスベガスの完敗がたまたまとんでもなく運が悪かったのか、それともブラックジャックの仕組み的にいつかは絶対に負けることが決まっていたのかが気になりだしました。そこでPythonの脱初心者の意味合いを含めて、CUIベースのブラックジャックのゲームとベーシックストラテジーを完璧にプレーイングするプログラムを実装して検証してみました。

#ブラックジャックのルールと完璧なプレーシングの意味
ブラックジャックのルールを全部詳細に説明しますと、それだけで違う記事が書けてしまうので簡潔にまとめてみました。ルール分かってるよって人はこの章中の完璧なプレーイング説明までとばしてください。

まず大原則としてこれだけを理解しておければいいことは、
・自分に配られたカードの目の合計が２１以下でディーラーより大きければ勝ったことになり、掛け金分が上乗せされて戻ってきます。しかし２１を超えてしまったらバースト扱いで掛け金が全没収されます。
・ディーラーは合計が１７以上になるまでは絶対にカードを引き続けなければいけません。もしディーラーがプレイヤーより２１に近ければ掛け金を全部没収することができます。しかしディーラー側も２１を超えてしまったらバースト扱いになり、今度はプレイヤーの掛け金が倍になって払い戻されます。
・１０以上の絵札カードはすべて値の１０として扱い、エースは１か１１扱いすることができます。２～９はそのままです。
・もしプレイヤーが最初に配られた２枚がエースと１０以上のカードの場合、合計が２１のためブラックジャックになり、掛け金分の1.5倍上乗せされ返ってきます
たったこれだけです。

ゲームの進行としましては、まず掛け金をかけたら、プレイヤーとディーラー互いに２枚配られます。しかしディーラーは１枚表のもう１枚は裏状態のため、ディーラーの正確な合計が分かりません。そこで常に先手のプレイヤーはディーラーの唯一表になっている１枚から推測して、スタンド（これ以上カードをいらない合図）をするまで以下の行動をとり続けないといけません（プレイヤーターン）。
・ヒット
もう１枚カードを引きたい
・（最初の２枚状態の時のみ）サレンダー
自分の最初の２枚が弱すぎるから降参する。だから掛け金を半分返して
・（最初の２枚状態の時のみ）ダブルダウン
どんなカードが来てもいいから１枚しか引かない。その代わり掛け金を倍にしてくれ
・（最初の２枚が同じ数字の時のみ）スプリット
それぞれに掛け金分を乗せるから、２枚を分割して独立した手として扱いたい。



そしてプレイヤーターンの間に合計が２１を超えてしまった場合は、ディーラーがどんなに弱い手でも負け扱いになり掛け金ボッシュートです。
もしプレイヤーが２１を超える前にスタンドした場合、ディーラーターンに入れ替わります。ここでは機械的にディーラーは１７以上になるまでカードを引き続けます。もしここで２１を超えたらプレイヤーの勝ちです。しかし１７以上２１以下の場合はプレイヤーとの目の合計勝負になり、２１に近いほうが勝ちます。もし引き分けの場合は掛け金がそのまま返ってきます。これを１ゲームとして、プレイヤーが飽きるか軍資金が全部溶けるまで回していきます。もちろん連続スプリットやインシュランスといった細かいルールはまだありますが詳細を知りたい方はぜひ自分で調べてみてください。


次に軽く完璧なプレーイングについて解説していきます。先ほどのルールを読むと、じゃあ自分は２１になるべく近づくようにカードを引いていけばいいのかと考えるかもしれませんがそれは場面によっては<b>間違い</b>です。なぜなら改めてディーラーターンの説明を読みますと、機械的に絶対１７以上になるまで引き続けないといけないからです。つまりディーラーの合計が２１を超えて自滅（バースト）するようにプレイヤーは立ち回ればいいのです。
確率の話になりますが、もしディーラーの２枚の合計が１６だった場合、次に引いてくるカードが５以下になる確率はいくつでしょうか？トランプ１デッキ（５２枚）の中には５以下のカードが２０枚あります。つまり２０/５２=５/１３です。逆に言えばディーラーがバーストする確率(５より大きいカード）は８/１３になります。そこで自分の初期２枚合計が１２、ディーラーが唯一表になっているのが６の場面を考えます（裏になっているカードは常に１０と考えます、つまりディーラーの合計は１６と仮定します）。自分が４/１３の確率で先に行動して自滅するよりはスタンドして、ディーラーに8/13の確率でバーストしてもらったほうが確率的にいい動きであるといえます。このように自分が２１に近くなくても、スタンドをしてディーラーに自滅してもらうのがブラックジャックにおいて最善手である場面がいくつか存在し、決まるとこの上なく気持ちいいです。
そして自分の合計とディーラーの表になっている目に応じて行うべき最善手を記したチートシートであるベーシックストラテジーチャートというものが存在します[1]。
<img width="204" alt="ベーシックストラテジーチャート" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1138601/7ab5ce9c-814d-66f3-3ecd-593fae45fbcc.gif">
これを完全に暗記することでブラックジャックの中級者に仲間入りしたと言い張ることができます。しかし今回の記事においてベーシックストラテジーチャートの動きを完璧なプレーイングと呼ぶことにしておりますが、実際はさらにもう１段階複雑なカウンティングという技術があります。そのためブラックジャック上級者からすると完璧なプレーイングじゃないとツッコミたくなると思いますが我慢してください。だって実装が大変なうえカジノ側に見つかると出禁を食らうからモチベーションが出ませんでした。
#コードの内容と結果
Qiitaを読んでいる層が、今更脱初心者のために書いた自分のコードから学ぶことは多分皆無だと思いますのでこちらには全コード載せませんし、解説もしません。興味がある方は自分のGithubを覗いてただければと思います(https://github.com/kkodio21/blackjack-game) 。
とりあえず苦労をしたところをいくつか挙げていきます。

<b>オブジェクトを使用するかどうか</b>
脱初心者のためにも、若干複雑と感じているクラスを使用してプレイヤーやディーラーの手を管理したいと意気込んでコーディングを考えていました。しかし途中でブラックジャックの性質上、配られる手をインスタンス化してもすぐ次のゲームではまた新しい手が配られるため恩恵がないと判断し、結局２次元配列を使用して進めていくことにしました。本当に脱初心者できているのかいまだによくわかっていないです。

<b>エースの扱い</b>
ブラックジャックにおいてはエースは状況に応じて１か１１扱いすることできます。つまり自分の２枚の手札がエースと７の場合は手札合計を８か１８と扱うことが来ます。もしここでディーラーの表が１０の場合、基本的には引くべきですがもしここで３枚目に８を引いた場合は２６ではなく、エースを１のほうで扱い合計１６で進めていきます。そしてこの状況に応じてエースの値を変えなければいけないという処理が難しく、最終的には最初にエースは値の１１として、引いてくるデッキに保存しました。そしてもしエースを持っている状態で２１を超えてしまったら自分の手に-１０を追加することでうまく実行できるようにしました。そのためプレイヤーの手を表示した場合に、実際のブラックジャックではあり得ない-１０という値が出てきてしまいますが、遊ぶ目的ではなく検証のためいいのではとこの方法で実装しました。もちろん使いづらいですがブラックジャックをCUI状態で遊べます。

```python:blackjackbot.py
if 11 in selfcards[a] and sum(selfcards[a]) > 21:
    selfcards[a].append(-10)
```

<b>連続スプリット</b>
ブラックジャックではたまに連続で同じ数字が来ることがあります。そして数字によっては連続でスプリットをするべき場面があります。例えば２枚とも８だった場合は絶対にスプリットを選ぶべきですが、もし独立した手にまた８が来てしまったらさらにスプリットをして、保有している手を３つにするといった場面が発生することがあります。著者も一度連続でエースが４回来て、連続スプリットで自分の目の前に４つの手があったときはパニクりました。なんせ最初の掛け金の４倍をかけたことになり、勝てば一攫千金、負ければ大損をしてしまうからです。結果はご想像にお任せします。
こちらも最初どのように実装するべきか悩みましたが、２次元配列に新しくその数字を要素として追加し、元の配列ではその数字を消せばいいだけでした。

<b>if地獄</b>
ゲームをしっかり実装できたところで、次は上の図のベーシックストラテジーチャートの動きをなぞるコードを書く必要性がありました。これは特段複雑ではなかったのですが、色ごとにif statementを書く必要性があり、ただただ条件分けを書いていくのが単調でつらかったです。

```python:blackjackbot.py
 if sum(selfcards[a])==11 or sum(selfcards[a])==10 and oppcards[0]<10:#double up
                    pickself(selfcards,a)
                    if 11 in selfcards[a] and sum(selfcards[a]) > 21:
                        selfcards[a].append(-10)
                    selfcards[a].append("up")  # if last is up dont hit
                elif sum(selfcards[a])==9 and (oppcards[0]<7 and oppcards[0]>2):
                    pickself(selfcards, a)
                    if 11 in selfcards[a] and sum(selfcards[a]) > 21:
                        selfcards[a].append(-10)
                    selfcards[a].append("up")  # if last is up dont hit
                elif (selfcards[a][0]==11 and selfcards[a][1]==6 or selfcards[a][0]==6 and selfcards[a][1]==11)\
                    and (oppcards[0]<7 and oppcards[0]>2):
                    pickself(selfcards, a)
                    if 11 in selfcards[a] and sum(selfcards[a]) > 21:
                        selfcards[a].append(-10)
                    selfcards[a].append("up")  # if last is up dont hit
                elif (selfcards[a][0]==11 and selfcards[a][1]==8 or selfcards[a][0]==8 and selfcards[a][1]==11)\
                    and oppcards[0]==6:
                    pickself(selfcards, a)
                    if 11 in selfcards[a] and sum(selfcards[a]) > 21:
                        selfcards[a].append(-10)
                    selfcards[a].append("up")  # if last is up dont hit
                elif (selfcards[a][0] == 11 and selfcards[a][1] == 7 or selfcards[a][0] == 7 and selfcards[a][1] == 11) \
                    and oppcards[0] < 7:
                    pickself(selfcards, a)
                    if 11 in selfcards[a] and sum(selfcards[a]) > 21:
                        selfcards[a].append(-10)
                    selfcards[a].append("up")  # if last is up dont hit
                elif (selfcards[a][0]==11 and (selfcards[a][1]==5 or selfcards[a][1]==4) or (selfcards[a][0]==5 or selfcards[a][1]==4)\
                      and selfcards[a][1]==11) and (oppcards[0]<7 and oppcards[0]>3):
                    pickself(selfcards, a)
                    if 11 in selfcards[a] and sum(selfcards[a]) > 21:
                        selfcards[a].append(-10)
                    selfcards[a].append("up")  # if last is up dont hit
                elif (selfcards[a][0] == 11 and (selfcards[a][1] == 3 or selfcards[a][1] == 2) or (selfcards[a][0] == 3 or selfcards[a][1] == 2)\
                      and selfcards[a][1] == 11) and (oppcards[0] < 7 and oppcards[0] > 4):
                    pickself(selfcards, a)
                    if 11 in selfcards[a] and sum(selfcards[a]) > 21:
                        selfcards[a].append(-10)
                    selfcards[a].append("up")  # if last is up dont hit
                else:
                    selfcards[a].append(0)
```


こんな感じでだいたい１２時間ぐらいかけて、コーディングを終えました。そしてついにブラックジャックで完璧なプレーイングをしたら儲かるのか検証をする時が来ました。最初の持ち金は１０００００で毎回の掛け金を１００にして実行しました。するとターミナルにこのような文字が目まぐるしく表れました。
![71600.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1138601/18851247-0bdf-0faf-73be-74f9f9b59440.png)


色々表示されていますが重要なのは　You have 71600。　うーん確実に最初の１０００００から減っている。。。　その後何度かリセットして実行してみましたが、最終的にすべてが所持金０になってしまいました。そこでMathplotlibを使用して、減り具合や何回目のプレー回数で０になるか検証してみたところ以下のグラフのようになりました。（縦：所持金　横：プレー回数）
１
![kakezentai.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1138601/22f973bc-a639-c679-766b-c35b7889a164.png)

２
![kekka2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1138601/4392a446-1114-ca39-e1e1-a51d79857555.png)

３
![kekka3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1138601/7212c5da-d7e8-ee8d-17fc-a4c44dca0d9d.png)


これらのグラフから主に２つのことが読み取れるかと思います。１つはブラックジャックは遊べば遊ぶほど損をすることです。今回の検証内容でもあった完璧なプレーイングの元ブラックジャックで儲けることができるに対しては完全にノーですね。持ち金の１０００分の１を賭け続けても３００００～６００００手らへんで所持金が０になります。
２つ目は必ずどこかで所持金が<b>一時的</b>に増えるタイミングがあることです。この一時的に増えるが重要で、たまたまそこの数回で儲かり続ける流れが来ただけで、長い視点で見ると最終的には損をします。特に１つ目のグラフの２００００手らへんからの上昇は目を見張るところがあります。実際に私もこのような上昇が最初の時にたまたま来ていただけで、このグラフの結果と同じように最終的には所持金が０になっています。さすがにハイリスクで回していたとはいえ２００手ぐらいで全ロスしているので運が悪いほうではあると思いますが。

#考察
ここまで読んでいただきありがとうございました。最後になぜブラックジャックが（データでは否定してしまいましたが）魅力的で世界中に愛されているギャンブルかについて軽く考察していきます。
それはズバリ<b>人間は愚かで微妙な変化には気づかない</b>からです。何を言ってんだと思うかもしれませんが、データがそれを体現しています。まずブラックジャックは１プレーするのに平均１分ほどかかるかといわれています。つまり持ち金の１００分の１をかけ続けたとしても、全部溶かすのに約５０００手、つまり８０時間ほどかかります。おそらく連続でこの時間を遊ぶ人はいないと思います。そこで仮に２時間遊ぶとして持ち金は1/４０＝２.５％しか減らないのです。この２.５％は感覚的には認知しづらい値かと思います。特にカジノではたまに動く大金のせいで一種の興奮状態になっており、２．５％の損は気にしなくなってしまいます。また金銭感覚が鈍るようわざとチップを使用しているので余計に損を認知しにくくしてます。うってつけにたまに大勝ちする要素があれば、これはブラックジャックに魅力を感じて依存症になる人が出てきてもおかしくないかと思います。損は感じず、儲けは印象に残ります。人は愚かですね。
以上からブラックジャックはベーシックストラテジーチャート通りの完璧なプレーイングをしても決して儲かることがなく、おそらく自分は今後リアルマネーで遊ぶことはないかと思います。

最近ポーカーをリアルマネーでやりたい欲が出てきてるのは秘密ですが。
KODIO



参考文献
[1] 4-Deck to 8-Deck Blackjack Strategy, https://wizardofodds.com/games/blackjack/strategy/4-decks/