テキサスホールデム

確率論

ゲームの基本ルール

 トランプ(英語ではPlaying Cardsでトランプと呼ぶのは日本語だけらしい)を使ったゲームでもっともよく知られているものの一つはポーカーだろう。通常のポーカーは、配られた5枚のカードを決められたルールに従って手札(Hole Card)を入れ替えることで、役(Hand)と言われる強弱を持つ組み合わせを作ることを目的とする。プレイヤーの中で最も強い役を作ったプレイヤーが勝者になる。

 テキサスホールデムが通常のポーカーと異なる点は、プレイヤーは手札を入れ替えることがない点である。代わりに配布される2枚の手札(Hole Card)と5枚の共通カード(Community Card)の合計7枚で作られる役を予想し、自分の手札が最も強い役であると予想する場合に賭け金(チップ)を多く投入し、他のプレイヤーと比較して弱い役であると予想される場合は賭け金を少なくしたり、ゲームを降りるという選択を行うことで、より多くの賭け金(チップ)を獲得することを目指すゲームである。プレイヤーができる選択は賭け金の選択のみで、カードの組み合わせ自体は配布されたカードのみで決定してしまうという特徴を持つ。ゲームの進行方法やプレイヤーのアクションに特別な名称がついていたりするため、理解に手間がかかるが、以下で簡単にテキサスホールデムの手順を説明しよう。

ディーラーボタンと強制的な賭け金(ブラインド)

 トランプのカードを配る人をディーラーというが、テキサスホールデムではディーラーの人が誰かを判別する方法として、プレイヤーにディーラーボタンというものを置き、他のプレイヤーと区別する。現実にはカードはプレイヤー自身が配るわけではなく、関係のない第三者(カジノのバニーちゃん)が配るのだろうが、どのプレイヤーがディーラーに相当するかということを目印として明らかにしておく。ディーラーボタンは時計回りなど決められたルールでプレイヤー間で順番に交代していくことになる。

 テキサスホールデムでは、強制的に賭け金を投入させられるプレイヤーが二人いる。ディーラーボタンがあるプレイヤーの左隣のプレイヤーをスモールブラインド、さらに左隣のプレイヤーをビックブラインドといい、ビックブラインドは最低の賭け金、スモールブラインドはその半分のチップを強制的にポット(投入されたチップを集めておく場所)に投入する。例えば、最低の賭け金がチップ10枚ならば、ビックブラインドは10枚、スモールブラインドは5枚を投入する。

 一般的には、スモールブラインドやビッグブラインドのプレイヤーは強制的にチップを投入させられるという意味で、不利な立ち位置であると言われる。また、ベッティングアクションはビックブラインドの左隣から初めて、最後はビッグブラインドのプレイヤーが行うという順(フロップ(2回目のベッティングラウンド)以降は、スモールブラインドから始めて、ディーラーボタンのプレイヤーが最後になる)で意思決定していくが、この順番は遅い方が有利であると言われてる。そのため、賭け金が強制されず、かつ、意思決定の順番が遅いディーラーボタンを持つプレイヤーが一番有利な立ち位置であると言われる。もちろん、この役回りは順番で担当するので短期決戦で勝負がつく場合を除けば、基本的にプレイヤー間に有利不利は生じないだろう。

ベッティング・ラウンド

 ベッティング・ラウンドと言われる賭けるチップの決定フェーズでは、プレイヤーは基本的に以下の3つのアクションから選択を行う。順番はビッグブラインドの左隣(2ラウンド(フロップ)以降はスモールブラインドのプレイヤー)から時計回りに行う。

  1. コール・・・現在の賭け金に等しくなるように追加のチップをポットに出して、プレイを継続する。
  2. フォールド・・・チップの支払いを拒否し、ゲームを降りる。すでに投入したチップは回収できない。
  3. レイズ・・・賭け金を引き上げ追加のチップをポットに出してプレイを継続する。現在の賭け金の2倍のレートを提示するのが一般的であるらしいが、任意の額に引き上げることができる。ただし、前回のレイズ金額より大きい金額を必ず引き上げなければならない(2倍ならこの条件を必ず満たす)。

レイズが行われた場合、そのプレイヤーを起点にもう一周ベッティング・ラウンドを行う。すべてのプレイヤーの賭金が等しくなった時点で、ベッティング・ラウンドは終了して次のフェーズに移行する。

 基本的なアクションは上記の3つであるが、例外的に次のアクションを選ぶこともできる。

  1. (オープニング)ベット・・・2回目以降のベッティング・ラウンドの最初のプレイヤーは賭け金が既にあるため、賭け金を投入しないことができる(チェックを選択することができる)が、追加的に賭け金を定めてチップをポットに投入することをベットという。
  2. チェック・・・2回目以降のベッティング・ラウンドの最初のプレイヤーは賭金を投入せずにプレイを継続することができ、そのアクションをチェックという。手前のプレイヤーがすべてチェックを選択した場合、プレイヤーはチェックを行うことができる。全員がチェックした場合は、追加の賭金なしにベッティング・ラウンドは終了して次のフェーズに移行する。
  3. オールイン・・・レイズが行われた結果、プレイヤーのすべてのチップを投入しても賭け金が足りない場合に、持っているチップをすべて投入することでプレイを継続することができる。オールインが行われた場合はオールインをしたプレイヤーだけ賭け金が異なることになる。オールインしたプレイヤーが勝利した場合、ポットの金額のうち賭けたチップと同額のチップだけを他のプレイヤーから受け取ることができ、残ったチップはオールインで勝利したプレイヤーを除いたプレイヤーのみで勝敗の判断をし、勝者に配分される。

漫画や映画で全部賭ける(オールイン)というと自信がある場合に強気に出ているかのように表現されることもあるが、現実には負けが込んでいてチップが足りない場合に、(チップが足りないけどこれが最後だから許せと)やむを得ず最後の勝負に出るのがオールインというアクションに相当している。

ゲームの進行手順

 ベッティング・ラウンドは1回のプレイで4回行われる。それぞれのフェーズには以下のような名称がついており、それぞれのフェーズでオープンされているカードの枚数が異なる。

  1. プリフロップ・・・手札(Hole Card)のみからベッティング・ラウンド
  2. フロップ・・・3枚の共通カード(Community Card)をオープンしてからベッティング・ラウンド
  3. ターン・・・追加で1枚の共通カードをオープンしてからベッティング・ラウンド
  4. リバー・・・最後の1枚の共通カードをオープンしてからベッティング・ラウンド

リバー終了時点で複数のプレイヤーが残っている場合、カードをすべてオープンして勝者であるプレイヤーを決定する。勝者はポットのチップをすべて受け取ることができる。このフェーズはショーダウンと言われる。ポーカーの役は強い順に以下のとおりである。

  1. ロイヤルストレートフラッシュ・・・同じ柄(Suit)で10,J,Q,K,Aのカードを揃える
  2. ストレートフラッシュ・・・同じ柄で連続した数値(Rank)のカードを5枚揃える
  3. フォーカード・・・同じ数値のカードを4枚揃える
  4. フルハウス・・・同じ数値のカードを3枚+2枚の2種類揃える
  5. フラッシュ・・・(数値が連続しない)同じ柄のカードを5枚揃える
  6. ストレート・・・(柄が異なる)連続した数値のカードを5枚揃える
  7. スリーカード・・・同じ数値のカードを3枚揃える
  8. ツーペア・・・同じ数値のカードを2枚+2枚の2種類揃える
  9. ワンペア・・・同じ数値のカードを2枚揃える
  10. ハイカード・・・役なし。最も数値が強いカードで強弱を比較する。

役が同じである場合は、数値(Rank)を比較してより強い順に決定する。テキサスホールデムでは

\[ \small 2\lt3\lt4\lt\cdots\lt J\lt Q \lt K\lt A \]

の順であり、スペード、ハート、ダイヤ、クローバーの絵柄(Suit)による強弱は存在しない。ストレートなどの連続した数値は循環してもよく、例えばQ,K,A,2,3などもストレートになる。それぞれの役で比較する数値は以下のとおりである。

  1. ストレート、ストレートフラッシュ・・・役に含まれるカードのうち連続する数値の最後の数値。例えばQ,K,A,2,3の場合は数値は3になる。
  2. フラッシュ・・・カードのうち最も大きな数値
  3. フォーカード、スリーカード、ワンペア・・・役のカードの数値
  4. フルハウス・・・役のうち3枚のカードの数値
  5. ツーペア・・・役のうち数値が大きい方の2枚のカードの数値

それでも決着がつかない場合は、以下のルールで比較する。

  1. フォーカード、スリーカード、ワンペア・・・役に含まれないカードのうち最も大きな数値
  2. フルハウス・・・役の2枚のカードの数値
  3. ツーペア・・・役のうち数値が小さい方の2枚のカードの数値。それも同じ場合は役に含まれないカードの数値

これでも優劣が決まらない場合は引き分け(タイと言われる)となり、引き分けのプレイヤー間でチップを均等配分する。ポットのチップが割り切れない場合は、端数のチップはスモールブラインドのプレイヤー(もしくは勝者のうち、ターンの順番が早く回ってきていたプレイヤー)が受け取るなどあらかじめ決められたルールで配分することになる。

カードの組み合わせの数

 トランプのカードの枚数は52枚であるから、その中から共通カード5枚と1プレイヤー当たりの手札2枚を引いた分だけカードの組み合わせが存在する。例えば、プレイヤーが4人の場合、考えうるカードの組み合わせは

\[ \small {}_{52}C_5 \times {}_{47}C_2 \times {}_{45}C_2 \times {}_{43}C_2 \times {}_{41}C_2 = 2.0595 \times 10^{18} \]

だけある(順番を変えて

\[ \small {}_{52}C_2\times{}_{50}C_2\times{}_{48}C_2\times{}_{46}C_2\times{}_{44}C_5 = 2.0595 \times 10^{18} \]

としても計算結果は同じである)。参加人数が増えるほど、この組み合わせは増えていくことになる。自分から見えるカードの組み合わせだけでも

\[ \small {}_{52}C_5\times{}_{47}C_2\ = 2809475760 \]

であり、28億通りあることになる。このことからテキサスホールデムはプレイヤーが全く同じ局面に出会うことが無いゲームであると言われている。言い換えれば、手札と共通カードの組み合わせから導かれる勝利確率を覚えておくというアプローチは現実的な方法論ではないということになるだろう(もちろん、人工知能やコンピュータを使ってもよい場合は別だろうけど)。

 プリフロップの2枚の段階では、1プレイヤーの手札の組み合わせは\(\small {}_{52}C_2=1326\)であるため、これは比較的容易に優位かどうかを判定できる。例えば、A,Aの2枚が最も優位性が高い組み合わせであると言われる。プリフロップの段階ではそれほどパターンがないため、プレイヤーによって戦略が変わることはあまりなく、多くの場合で定石どおりにプレイされることが通常であろう。本やウェブサイトで確率による解説がなされるのはこのあたりまでであることが多いように思える。フロップの段階で

\[ \small {}_{52} C_2 \times {}_{50} C_3 = 25989600 \]

通りにもなるため、優位性が高い組み合わせであるかはある程度感覚的に推測していくしかなくなるだろう。確率の計算をしてもあまり一般論を語ることができないため、本やウェブサイトでは経験則的な解説に終始する場合がほとんどであるように見受けられる。

なぜこのゲームが気になるのか?

 娯楽以外の理由でギャンブルをしたりカジノに行くことを筆者はお薦めしないし、人間の脳にはいくつか顕著な不具合があるために、それを利用することでギャンブルに嵌まるように仕向ける仕組みをカジノやギャンブル場はいくつも備えている。それでもこのゲームが気になるのは、このゲームが機械学習や人工知能(AI)の研究・応用として重要な基本的要素を多く持っているためである。(chatGPTいわく)実際にOpenAIではDeepStack、FacebookではPluribusという名称でテキサスホールデムに関する人工知能のアルゴリズムを研究していたらしい。人工知能の研究において重要な基本的要素を具体的にいうと以下のとおりである。

  1. 発生し得る状況が天文学なオーダーで存在しており、すべてのパターンを総当たりして確率や最適な戦略を計算することが現実的ではないため、部分的なサンプリングに従って学習することで戦略を構築する必要がある(深層学習による汎化性能の獲得の事例になっている)。
  2. ゲームの進行に伴った状態遷移(カードの開示、ベットの推移)がある(強化学習による戦略構築を用いることができる)。
  3. 他のプレイヤーとの競争関係がある(自己強化学習や敵対的生成ネットワークのようなテクニックを適用することができる)。
  4. 不完全な情報を元に意思決定を行う必要がある。

1.~3.は囲碁や将棋でも満たされる条件であるが、4.の条件を満たしていないため、これらのゲームは完全情報ゲームと言われる。完全情報ゲームの大きな特徴は不敗戦略(最悪の状況でも負けることがない戦略)が存在することが理論的に証明されることである。これはより多くのデータサンプルを学習した方がより強い人工知能になることを意味する。

 一方で、テキサスホールデムのような不完全情報ゲームでは必勝戦略や不敗戦略というものは存在しないため、どれだけ合理的な意思決定を行っても負ける可能性が残るという意味において完璧な戦略というものが存在しないということに注意する必要がある。言い換えれば、学習したら学習しただけ強くなるわけではなく(もちろんそういった要素はあるけれど)、プレイヤー間の相性みたいなものが勝敗を左右する要素として現れることになる。この場合、相手のベット戦略から相手の性格を推測するようなルールも学習する必要があるのかもしれない。

 以上のように、テキサスホールデムは機械学習や人工知能の学習や研究をする上で良い題材に見えるため、これを活用してみてはどうだろうか。株やFXなど金融市場の取引も基本的には不完全情報ゲームであり、囲碁や将棋よりテキサスホールデムに近い性質を持っていることは指摘しておいてもよいだろう。ちなみにテキサスホールデムはチップを多く持っている(スタックが大きい)プレイヤーの方が戦略上有利になる傾向がある(他のプレイヤーをオールインに追いやることができる)。金持ちほど金融市場では有利ということと共通しているかもしれない(おおっと・・・)。

 おそらく、一般的な市民は人を騙すという行為に慣れていなかったり、罪悪感を覚えることが多いため、このゲームに慣れていないプレイヤーは正直に手札が強い場合に強気に出て、手札が弱い場合にフォールドするという傾向が顕著に表れるし、他のプレイヤーが同様の戦略で行動しているかのように予想すると推測される。もちろん、その戦略でも感覚的な確率計算が適切であれば相応に強いプレイヤーであると考えられるが、慣れたプレイヤーというのはそういったプレイヤーの戦略を切り崩す方策(ブラフなど情報を操作する戦略)を持っているものなのだろう。筆者自身も興味が湧くようであれば、もう少しこのゲームの性質を調べてみようかと思う。

おまけ:ポーカーの役の判定アルゴリズム

 オープンソースのソースコードを見ていると、ポーカーの役の判定に素数を用いているケースがあった。例えば、絵柄のスペード、ハート、ダイヤ、クラブに\(\small 2,3,5,7\)と数値を割り当て、カードの数値\(\small 2,3,\cdots,J,Q,K,A\)に\(\small 11,13,\cdots,43,47,53,59\)をそれぞれ割り当てて、それらの掛け算でカードを表現する。5枚のカードの合成はそれらカードを表現する数値の掛け算で表現するという方法である。テキサスホールデムではフラッシュの判定以外に絵柄を使わないため、このような表現方法が可能なのかもしれない。

 この方法だと、フラッシュやストレートの判定を比較的容易に行うことができる。例えば、5枚のカードの合成値が\(\small 2^5,3^5,5^5,7^5\)のいずれかで割り切れればフラッシュであるし、ストレートは\(\small 11\times13\times17\times19\times23\)などあらかじめ13個の配列を用意しておいて、それで割り切れればストレートであると判定できることになる。どういったアルゴリズムが効率的な判定方法であるかは何らかの研究や方法論があるのかもしれない。

 お試しにpythonで実装してみた。コードがポンコツで参考にならないという批判は聞かないことにする。ストレートの判定だけ素数を使って行っている。

class Card:
    SUIT_CHAR = ['S','H','D','C']
    #大小比較のため、Aを最後にする。
    RANK_CHAR = ['2','3','4','5','6','7','8','9','T','J','Q','K','A']
    #ストレート判定用の素数
    RANK_INDEX = [2,3,5,7,11,13,17,19,23,29,31,37,41]
    
    def __init__(self, suit, rank):
        self.suit = suit
        self.rank = rank
        
    def string_value(self):
        return self.SUIT_CHAR[self.suit] + self.RANK_CHAR[self.rank] 
        
    def prime_value(self):
        return self.RANK_INDEX[self.rank]

class Hand:
    #cardsはCard型5つのlist
    def __init__(self, cards):
        self.rank_count = [0] * 13
        self.suit_count = [0] * 4
        self.prime_value = 1
    
        for c in cards:
            self.prime_value *= c.prime_value()
            self.rank_count[c.rank] += 1
            self.suit_count[c.suit] += 1

class Evaluator:
    #弱い順でストレートのチェックをする。数値しか使わないので値が等しい場合のみストレートになる。
    STRAIGHT_CHECK = [2727566,282162,45510,8610,2310,15015,85085,323323,\
                      1062347,2800733,6678671,14535931,31367009]
    
    def __init__(self):
        pass
        
    def eval(self, hand):
        strength = [0] * 4
        
        #ストレートとフラッシュのチェック(数値がすべてバラバラなので、countの最大値が1の場合のみチェック)
        if max(hand.rank_count) == 1:
            bFlush = False
            if max(hand.suit_count) == 5:
                bFlush = True
                
            bStraight = False
            if hand.prime_value in self.STRAIGHT_CHECK:
                bStraight = True
                
            if bFlush and bStraight:
                strength[0] = 8
                strength[1] = self.STRAIGHT_CHECK.index(hand.prime_value)
                return strength
            elif bFlush:
                strength[0] = 5
                strength[1] = len(hand.rank_count) - list(reversed(hand.rank_count)).index(1) - 1
                return strength
            elif bStraight:
                strength[0] = 4
                strength[1] = self.STRAIGHT_CHECK.index(hand.prime_value)
                return strength
            else:
                #役なし
                strength[0] = 0
                strength[1] = len(hand.rank_count) - list(reversed(hand.rank_count)).index(1) - 1
                return strength
        
        #ワンペアかツーペア
        if max(hand.rank_count) == 2:
            l = hand.rank_count.index(2)
            r = len(hand.rank_count) - list(reversed(hand.rank_count)).index(2) - 1
            if l == r:
                #ワンペア
                strength[0] = 1
                strength[1] = l
                strength[2] = len(hand.rank_count) - list(reversed(hand.rank_count)).index(1) - 1
                return strength
            else:
                #ツーペア
                strength[0] = 2
                strength[1] = r
                strength[2] = l
                strength[3] = hand.rank_count.index(1)
                return strength
            
        #スリーカードかフルハウス
        if max(hand.rank_count) == 3:
            if 2 in hand.rank_count:
                #フルハウス
                strength[0] = 6
                strength[1] = hand.rank_count.index(3)
                strength[2] = hand.rank_count.index(2)
                return strength
            else:
                #スリーカード
                strength[0] = 3
                strength[1] = hand.rank_count.index(3)
                strength[2] = len(hand.rank_count) - list(reversed(hand.rank_count)).index(1) - 1
                return strength
            
        #フォーカード
        strength[0] = 7
        strength[1] = hand.rank_count.index(4)
        strength[2] = hand.rank_count.index(1)
        return strength
    
    # 1が勝ちの場合1, 2が勝ちの場合-1, 引き分けの場合0を返す。
    def compare(self, hand1, hand2):
        s1 = self.eval(hand1)
        s2 = self.eval(hand2)
        for i in range(0, 4):
            if s1[i] > s2[i]:
                return 1
            elif s1[i] < s2[i]:
                return -1
        
        return 0

if __name__ == "__main__":
    e = Evaluator()
    cards = [Card(0, 8),Card(0, 9),Card(0, 10),Card(0, 11),Card(0, 12)]
    hand = Hand(cards)
    print(e.eval(hand))