Mukai Systems

Baltroとポーカーハンドの確率

大谷選手の専属通訳である水原さんが大谷選手の金を着服して違法賭博していたということで世間を驚かせた。Wikipediaによると少なくとも6億8000万円相当着服していたというから衝撃だ。

私も過去に気の知れた友達と、おもちゃのチップを賭けてTexas hold 'emを一日中やったことがある。その日はドーパミンが大量に放出されるのを感じた。これが現金だとしたら取り返しがつかなくなるような遊びだなということがよくわかったので、それ以降は、競馬や競輪やパチンコや宝くじなど、賭け事は絶対にしないというルールを作って守っている。勿論、期待値が1を上回る場合だったらその限りではないけどね笑

ところで、私はここ最近、寝る間も惜しんで楽しんでいるゲームがある。

大谷選手のために大谷ルールが追加されたように、私も賭け事に対してルールを追加するときが来たようだ。

誰にも迷惑が掛からない賭け事はしてもよい。Baltroルールだ。

Baltroは一言でいえば、なんでもありのポーカーだ。2024年の2月に突如現れ、Steamで圧倒的に好評の評価を得ている。これを見るに、たくさんのギャンブル依存症患者を量産しているに違いない。実は任天堂ストアでも販売することが決定されていたが、政治的な理由でストアページが削除されてしまったという記事を読んだ。ほかにもポーカーのゲームなどはあり、賭け事だから削除したという説明では筋が通らないのだという。この判断について、一貫性を欠いてはいるものの任天堂は正しかったかもしれない。なぜなら、キッズには刺激が強すぎるゲームだからだ。

3回のブラインドからなるアンティを8回クリアすることがこのゲームの目標である。ブラインドでは必要なチップが提示されるので、それを4回のハンドと4回のディスカード(手札の交換)で稼がなければならない。

回を重ねるごとにブラインドで稼がなければならないチップが増えていくのだが、各ブラインドの間にはショップがあって次のようなものを購入してたくさんチップを稼ぐための手段が提供される。

惑星は使用することによって永続的に役を強化する消耗品である。地球ならFull Houseといったように、役ごとに存在している。

初期の倍率は役の確率順に従っているのだが、この仕組みによって、One PairがStraight Flushよりチップを稼げる!といった驚きのビルドが可能となる。

タロットは使用することによって様々な効果を得られる消耗品である。カードのsuitを特定のsuitに変更してFlushを狙いやすくしたり、不要なカードをデッキから取り除いたりする。

バウチャーは購入することによって様々な永続効果を得られる。ブラインドごとのハンドの回数やディスカードの回数を増やしたり、ショップの商品を割引したりする。

ジョーカーはトランプにおけるワイルドカードではない。Baltroでは購入することによって保持している間様々な効果を得られる。5枚まで持つことができて、特定のsuitやrankが役に含まれていた時にチップの量を増やしたり、特定の条件を満たしたときにタロットを生成する効果を持つものなどがある。

ブースターパックは、惑星やタロットなどの種類ごとに存在していて、開封するまで何が入っているか分からない。いわゆるガチャ要素で、これがドーパミンの放出をさらに促す仕組みになっている。Cookie ClickerやVampire Survivors等、この手のゲームはこの辺の演出が細かいなと感心してしまう。

相当に説明は端折っているが、概要はこんな感じである。これらの様々な変数によって、思いもよらない相乗効果が発生する。あなたはこのゲームを買ってしまったら最後。誰にも迷惑はかけることはないかもしれないが、最も高価なもの——時間——をAll-inすることになる。

Baltroでは手札の枚数は8枚である。ここから5枚まで選択して役を作る。Flush FiveやFlush Houseなど見慣れない役はあるものの、その実体は名前から明らかだし、ポーカーをやったことがある人だったらすぐに遊べるようになっている。

さて、5枚の場合に比べて8枚の場合はどのように役を作る確率が変わるのだろうか。任意のデッキから任意の枚数を引いた時に作れる役の確率をシミュレーションするプログラムを作ってみた。

カードはlisperのやり方、即ちlistで表現するとしよう。例えば、スペードのエースは(A ♠)のようになる。このようなデータ構造のためのアクセサを二つ作る。

(defun card-rank (card)
  (car card))

(defun card-suit (card)
  (cadr card))
* (card-rank '(A ♠))
A
* (card-suit '(A ♠))
♠

そうすると、ハンドはカードのリストとして自然に表現できるので、例えばRoyal Flushは((A ♠) (K ♠) (Q ♠) (J ♠) (10 ♠))のように表現される。

デッキはsuitとrankの直積で表すことができるので、次のようなコードでデッキが構築できる。

(defparameter *ranks* '(A K Q J 10 9 8 7 6 5 4 3 2))
(defparameter *suits* '(♠ ♥ ♣ ♦))

(defparameter *deck* (apply #'concatenate
                            (cons 'list
                                  (mapcar (lambda (x)
                                            (mapcar (lambda (y)
                                                      (list x y))
                                                    *suits*))
                                          *ranks*))))
* *deck*
((A ♠) (A ♥) (A ♣) (A ♦) (K ♠) (K ♥) (K ♣) (K ♦) (Q ♠) (Q ♥) (Q ♣) (Q ♦) (J ♠)
 (J ♥) (J ♣) (J ♦) (10 ♠) (10 ♥) (10 ♣) (10 ♦) (9 ♠) (9 ♥) (9 ♣) (9 ♦) (8 ♠)
 (8 ♥) (8 ♣) (8 ♦) (7 ♠) (7 ♥) (7 ♣) (7 ♦) (6 ♠) (6 ♥) (6 ♣) (6 ♦) (5 ♠) (5 ♥)
 (5 ♣) (5 ♦) (4 ♠) (4 ♥) (4 ♣) (4 ♦) (3 ♠) (3 ♥) (3 ♣) (3 ♦) (2 ♠) (2 ♥) (2 ♣) (2 ♦))

ここまで、データ構造が定まればハンドの役を判定する関数は容易に実装できる。効率を無視して、いくつかの補助関数を用意すれば自然言語のように表現できる。

(defun unique-ranks (hand)
  (remove-duplicates (mapcar #'card-rank hand)))

(defun most-frequent-rank-count (hand)
  (apply #'max (mapcar (lambda (x)
                         (count x hand :key #'card-rank))
                       (unique-ranks hand))))

(defun flush-five-p (hand)
  (and (five-of-a-kind-p hand) (flush-p hand)))

(defun flush-house-p (hand)
  (and (full-house-p hand) (flush-p hand)))

(defun royal-flush-p (hand)
  (and (straight-flush-p hand) (subsetp '(A 10) (unique-ranks hand))))

(defun five-of-a-kind-p (hand)
  (= (most-frequent-rank-count hand) 5))

(defun straight-flush-p (hand)
  (and (straight-p hand) (flush-p hand)))

(defun four-of-a-kind-p (hand)
  (>= (most-frequent-rank-count hand) 4))

(defun full-house-p (hand)
  (and (three-of-a-kind-p hand) (= (length (unique-ranks hand)) 2)))

(defun flush-p (hand)
  (= (length (remove-duplicates (mapcar #'card-suit hand))) 1))

(defun straight-p (hand)
  (let ((ranks (unique-ranks hand)))
    (find-if (lambda (straight-ranks)
               (null (set-exclusive-or straight-ranks ranks)))
             *straight-ranks*)))

(defun three-of-a-kind-p (hand)
  (>= (most-frequent-rank-count hand) 3))

(defun two-pair-p (hand)
  (<= (length (unique-ranks hand)) (- (length hand) 2)))

(defun one-pair-p (hand)
  (< (length (unique-ranks hand)) (length hand)))
* (defparameter *hand* '((A ♥) (A ♥) (A ♥) (A ♥) (A ♥)))
*HAND*
* (flush-five-p *hand*)
T
* (five-of-a-kind-p *hand*)
T
* (flush-p *hand*)
T
* (full-house-p *hand*)
NIL

最後に、組み合わせに対して関数を呼ぶ高階関数を定義すれば52枚のデッキから5枚すべての組み合わせに対しての処理を行えるようになる。

(defun each-combination (fn lis r)
  (let ((n 0))
    (labels ((rec (lis r acc)
                  (cond ((zerop r)
                         (funcall fn (reverse acc))
                         (incf n))
                        ((null lis) nil)
                        (t
                          (rec (cdr lis) (1- r) (cons (car lis) acc))
                          (rec (cdr lis) r acc)))))
      (rec lis r nil)
      n)))

例えば52枚のデッキから5枚を選択する組合せに対してprint関数を呼び出すと、全 \( {}_{52} \mathrm{ C }_5 = 2598960 \) 通りの手札を出力することができる。

* (each-combination #'print *deck* 5)
((A ♠) (A ♥) (A ♣) (A ♦) (K ♠))
((A ♠) (A ♥) (A ♣) (A ♦) (K ♥))
((A ♠) (A ♥) (A ♣) (A ♦) (K ♣))
((A ♠) (A ♥) (A ♣) (A ♦) (K ♦))
((A ♠) (A ♥) (A ♣) (A ♦) (Q ♠))
((A ♠) (A ♥) (A ♣) (A ♦) (Q ♥))
((A ♠) (A ♥) (A ♣) (A ♦) (Q ♣))
((A ♠) (A ♥) (A ♣) (A ♦) (Q ♦))
((A ♠) (A ♥) (A ♣) (A ♦) (J ♠))
((A ♠) (A ♥) (A ♣) (A ♦) (J ♥))
((A ♠) (A ♥) (A ♣) (A ♦) (J ♣))
((A ♠) (A ♥) (A ♣) (A ♦) (J ♦))
...

これで、すべての組み合わせの役を集計する準備ができた。まずは、一般的な52枚のデッキから5枚引いた時の場合を確認してみよう。

(:HAND FIVE-OF-A-KIND :FREQUENCY 0 :PROBABILITY 0.0)
(:HAND FLUSH-HOUSE :FREQUENCY 0 :PROBABILITY 0.0)
(:HAND FLUSH-FIVE :FREQUENCY 0 :PROBABILITY 0.0)
(:HAND ROYAL-FLUSH :FREQUENCY 4 :PROBABILITY 1.5390772e-6)
(:HAND STRAIGHT-FLUSH :FREQUENCY 36 :PROBABILITY 1.3851694e-5)
(:HAND FOUR-OF-A-KIND :FREQUENCY 624 :PROBABILITY 2.4009604e-4)
(:HAND FULL-HOUSE :FREQUENCY 3744 :PROBABILITY 0.0014405763)
(:HAND FLUSH :FREQUENCY 5108 :PROBABILITY 0.0019654015)
(:HAND STRAIGHT :FREQUENCY 10200 :PROBABILITY 0.003924647)
(:HAND THREE-OF-A-KIND :FREQUENCY 54912 :PROBABILITY 0.021128451)
(:HAND TWO-PAIR :FREQUENCY 123552 :PROBABILITY 0.047539014)
(:HAND ONE-PAIR :FREQUENCY 1098240 :PROBABILITY 0.42256904)
(:HAND HIGH-CARD :FREQUENCY 1302540 :PROBABILITY 0.5011774)

これはWikipediaに記載されている確率と同じだからプログラムは正しそうだ。無作為に5枚選ぶだけだと、9割はOne PairかHigh Cardなのね。

52枚のデッキから8枚引いた時の場合は次のようになった。

(:HAND FIVE-OF-A-KIND :FREQUENCY 0 :PROBABILITY 0.0)
(:HAND FLUSH-HOUSE :FREQUENCY 0 :PROBABILITY 0.0)
(:HAND FLUSH-FIVE :FREQUENCY 0 :PROBABILITY 0.0)
(:HAND ROYAL-FLUSH :FREQUENCY 64860 :PROBABILITY 8.618832e-5)
(:HAND STRAIGHT-FLUSH :FREQUENCY 546480 :PROBABILITY 7.261825e-4)
(:HAND FOUR-OF-A-KIND :FREQUENCY 2529262 :PROBABILITY 0.003360975)
(:HAND THREE-OF-A-KIND :FREQUENCY 38493000 :PROBABILITY 0.051150896)
(:HAND FULL-HOUSE :FREQUENCY 45652128 :PROBABILITY 0.060664203)
(:HAND FLUSH :FREQUENCY 50850320 :PROBABILITY 0.06757175)
(:HAND HIGH-CARD :FREQUENCY 53476080 :PROBABILITY 0.071060956)
(:HAND STRAIGHT :FREQUENCY 67072620 :PROBABILITY 0.08912853)
(:HAND ONE-PAIR :FREQUENCY 236092500 :PROBABILITY 0.31372827)
(:HAND TWO-PAIR :FREQUENCY 257760900 :PROBABILITY 0.34252203)

High CardがStraightよりもレアだって!?

Royal FlushやStraight Flushも一桁確率が上がっている。ディスカードも含めるとFull Houseくらいなら余裕で狙えそうというのが直感的に正しい。

ん-。それにしても遅い。

52枚から5枚選ぶ場合のプログラムの実行は数秒程度だったが、8枚選ぶ場合は丸一日程度かかってしまった。それでもまったく問題ない。Baltroをしてれば勝手に時間がすぎるから笑

52枚から8枚選んだ時のすべての役の検証はさらにそこから5枚選ぶ場合の中で最も強い役を決定する必要があるので、 \( {}_{52} \mathrm{ C }_8 \times {}_8 \mathrm{ C }_5 = 42142136400 \) 組のハンドを試行しなければならない!

実はBaltroには二種類のsuitだけからなるデッキなど、様々なデッキがあったりする。いろいろ変数を変えて遊んでみてね!

Source code

naive版では処理が遅すぎたのでより無駄のないfast版で実際の計算は行った。いずれも52枚のデッキから手札が5枚の場合の計算はできる。

Steel Bank Common Lispで実行した。

$ sbcl --version
SBCL 2.3.2

Notes

ポーカーではAの扱いは若干特殊だ。例えば、StraightではA, K, Q, J, 105, 4, 3, 2, Aというように114のように振る舞う。

More info

See also