ポーカーの役ができる確率を計算してみた（ジョーカー１枚）

[ジョーカーを1枚加えて合計53枚のトランプカードを使用した場合の、ポーカーの役ができる確率を計算してみました。](https://github.com/anonymously115/repository115/blob/main/poker.py)
- - -
- ###### 準備
```poker.py
"""
ジョーカーを1枚加えて合計53枚のトランプカードを使用した場合の、
ポーカーの役ができる確率を計算する。
"""
# ジョーカー及び13*4枚のカードを用意する
JOKER=(0,0)
CARDS=[JOKER]+[(i%4+1,i//4+1) for i in range(52)]

# 全パターン数
nPatterns=0

# ファイブカードのパターン数
cntFiveOfAKind=0

# ロイヤルストレートフラッシュのパターン数
cntRoyalStraightFlush=0

# ストレートフラッシュのパターン数
cntStraightFlush=0

# フォーカードのパターン数
cntFourOfAKind=0

# フルハウスのパターン数
cntFullHouse=0

# フラッシュのパターン数
cntFlush=0

# ストレートのパターン数
cntStraight=0

# スリーカードのパターン数
cntThreeOfAKind=0

# ツーペアのパターン数
cntTwoPair=0

# ワンペアのパターン数
cntOnePair=0

# ノーペア（ハイカード）のパターン数
cntNoPair=0
```
- - -
- ###### ファイブカードの判定
:::note
以下、引数handにおいて、
$0\le i<j\le4$に対して$0\le H_{i,1}\le H_{j,1}\le4$
$i>0$ならば$H_i\neq \text{JOKER}$
（`0<=i<j<=4`に対して`hand[i][1]<=hand[j][1]`，
　`i>0`ならば`hand[0]!=JOKER=(0,0)`）
を前提とします。
:::
```py
def isFiveOfAKind(hand):
    """ファイブカードの判定。
    ・手札にジョーカーがある
    　かつ
    ・残り4枚の数字が全て同じ
    場合、True。
    """
    return hand[0]==JOKER and hand[1][1]==hand[2][1]==hand[3][1]==hand[4][1]
```
- - -
- ###### ロイヤルストレートフラッシュの判定
```py
def isRoyalStraightFlush(hand):
    """ロイヤルストレートフラッシュの判定
    手札にジョーカーがある場合
    ・残り４枚のスートが全て同じ
    　かつ
    ・残り４枚の数字が(A,10,J,Q,K)の中の４個で構成されている
    場合、True。
    手札にジョーカーがない場合
    ・５枚のスートが全て同じ
    　かつ
    ・５枚の数字が(A,10,J,Q,K)で構成されている
    場合、True。
    """
    if hand[0]==JOKER: #ジョーカーが手札にある場合
        if not hand[1][0]==hand[2][0]==hand[3][0]==hand[4][0]:
            #異なるスートがある場合、ロイヤルストレートフラッシュでない
            return False
        #残り4枚の数字が(A,10,J,Q),(A,10,J,K),(A,J,Q,K),(10,J,Q,K)ならば
        #ロイヤルストレートフラッシュ成立
        return tuple(hand[i][1] for i in range(1,len(hand))) in [(1,10,11,12),(1,10,11,13),(1,10,12,13),(1,11,12,13),(10,11,12,13)]
    else:
        if hand[0][1]!=1:
            #0枚目がエースでない場合、ロイヤルストレートフラッシュでない
            return False
        for i in range(1,len(hand)):
            if hand[i][0]!=hand[0][0] or hand[i][1]!=9+i:
                #スートが異なる場合、または
                #1～4枚目が(10,J,Q,K)でない場合、即ちi枚目が9+iでない場合
                #ロイヤルストレートフラッシュでない
                return False
        return True
```
- - -
- ###### ストレートフラッシュの判定
:::note
(JOKER,10,J,Q,K)の場合もTrueを返却しますが、後の判定順序で重複カウントしないように制御します。
:::
```py
def isStraightFlush(hand):
    """ストレートフラッシュの判定
    手札にジョーカーがある場合※ジョーカーを0枚目とカウント
    ・残り４枚のスートが全て同じ
    　かつ
    ・(2枚目の数字-1枚目の数字,3枚目の数字-1枚目の数字,4枚目の数字-1枚目の数字)=(1,2,3),(1,2,4),(1,3,4),(2,3,4)のいずれかの
    　（または、(2枚目の数字-1枚目の数字,3枚目の数字-2枚目の数字,4枚目の数字-3枚目の数字)=(1,1,1),(1,1,2),(1,2,1),(2,1,1)のいずれかの）
    場合、True。
    手札にジョーカーがない場合
    ・５枚のスートが全て同じ
    　かつ
    ・1≦i≦4に対して、i枚目の数字=0枚目の数字+iの
    　（または、i枚目の数字=i-1枚目の数字+1）の
    場合、True。
    """
    if hand[0]==JOKER: #ジョーカーが手札にある場合
        if not hand[1][0]==hand[2][0]==hand[3][0]==hand[4][0]:
            #異なるスートがある場合、ストレートフラッシュでない
            return False
        #i枚目と1枚目の数字の差で判定
        return tuple(hand[i][1]-hand[1][1] for i in range(2,len(hand))) in [(1,2,3),(1,2,4),(1,3,4),(2,3,4)]
        #（別解）i枚目とi-1枚目の数字の差で判定
        #return tuple(hand[i][1]-hand[i-1][1] for i in range(2,len(hand))) in [(1,1,1),(1,1,2),(1,2,1),(2,1,1)]
    else:
        for i in range(1,len(hand)):
            if hand[i][0]!=hand[0][0] or hand[i][1]!=hand[0][1]+i:
                #異なるスートがある、またはi枚目と0枚目の数字の差がiでない場合、
                #ストレートフラッシュでない
                return False
        return True
```
- - -
- ###### フォーカードの判定
:::note
JOKERが手札にある場合、ファイブカードが成立する場合もTrueを返却しますが、後の判定順序で重複カウントしないように制御します。
:::
```py
def isFourOfAKind(hand):
    """フォーカードの判定
    手札にジョーカーがある場合
    ・残り４枚中３枚の数字が同じならばTrue。
    手札にジョーカーがない場合
    ・５枚中４枚の数字が同じならばTrue。
    """
    if hand[0]==JOKER: #手札にジョーカーがある場合
        #残り４枚中３枚の数字が同じならばフォーカード
        return hand[1][1]==hand[2][1]==hand[3][1]\
            or hand[2][1]==hand[3][1]==hand[4][1]
    else:
        #５枚中４枚の数字が同じならばフォーカード
        return hand[0][1]==hand[1][1]==hand[2][1]==hand[3][1]\
            or hand[1][1]==hand[2][1]==hand[3][1]==hand[4][1]
```
- - -
- ###### フルハウスの判定
:::note
JOKERが手札にある場合、残り4枚中3枚が同じ数字の場合、フォーカードが成立しますが、フルハウスの判定でもTrueを返却することにしています。
4枚同じ数字の場合は勿論ファイブカードです。
後の判定順序で重複カウントしないように制御します。
:::
```py
def isFullHouse(hand):
    """フォーカードの判定
    手札にジョーカーがある場合
    ・残り４枚中３枚の数字が同じ（フルハウスと言えなくもないが、フォーカードに吸収される）
    　または
    ・残り４枚中２枚の数字が同じ、更に残りの２枚の数字が同じ
    場合、True。
    手札にジョーカーがない場合
    ・５枚中３枚の数字が同じ、更に残りの２枚の数字が同じならばTrue。
    """
    if hand[0]==JOKER: #手札にジョーカーがある場合
        #3行ある判定条件のうち、上2行はフォーカードと同じ
        return hand[1][1]==hand[2][1]==hand[3][1]\
            or hand[2][1]==hand[3][1]==hand[4][1]\
            or hand[1][1]==hand[2][1] and hand[3][1]==hand[4][1]
    else: #手札にジョーカーがない場合
        return hand[0][1]==hand[1][1]==hand[2][1] and hand[3][1]==hand[4][1]\
            or hand[0][1]==hand[1][1] and hand[2][1]==hand[3][1]==hand[4][1]
```
- - -
- ###### フラッシュの判定
:::note
ロイヤルストレートフラッシュ、ストレートフラッシュの場合もTrueを返却しますが、後の判定順序で重複カウントしないように制御します。
:::
```py
def isFlush(hand):
    """フラッシュの判定
    ・ジョーカーを除くカードが全て同じスートの場合、True。
    """
    j=1 if hand[0]==JOKER else 0
    for i in range(j+1,len(hand)):
        if hand[i][0]!=hand[j][0]:
            return False
    return True
```
- - -
- ###### ストレートの判定
:::note
ロイヤルストレートフラッシュ、ストレートフラッシュの場合もTrueを返却しますが、後の判定順序で重複カウントしないように制御します。
:::
```py
def isRoyalStraight(hand):
    """ストレート(10-J-Q-K-A型)の判定
    手札にジョーカーがある場合
    ・残り４枚の数字が(A,10,J,Q,K)の中の４個で構成されている
    　または
    ・残り４枚中、最小の数字以外の３枚と最小の数字との差が
    　(1,2,3),(1,2,4),(1,3,4),(2,3,4)でいずれかの
    （または、４枚を数字で昇順ソートした時、隣り合う２枚の差が
    　(1,1,1),(1,1,2),(1,2,1),(2,1,1)のいずれかの）
    場合、True。
    手札にジョーカーがない場合
    ・５枚の数字が(A,10,J,Q,K)で構成されている
    または
    ・i枚目と0枚目の数字の差がi（または、i枚目とi-1枚目の数字の差が1）
    ならばTrue。
    """
    if hand[0]==JOKER:
        return tuple(hand[i][1] for i in range(1,len(hand))) in [(1,10,11,12),(1,10,11,13),(1,10,12,13),(1,11,12,13),(10,11,12,13)]
    else:
        if hand[0][1]!=1:
            return False
        for i in range(1,len(hand)):
            if hand[i][1]!=9+i:
                return False
        return True

def isStraight(hand):
    if isRoyalStraight(hand):
        return True
    if hand[0]==JOKER:
        return tuple(hand[i][1]-hand[1][1] for i in range(2,len(hand))) in [(1,2,3),(1,2,4),(1,3,4),(2,3,4)]
    else:
        for i in range(1,len(hand)):
            if hand[i][1]!=hand[0][1]+i:
                return False
        return True
```
- - -
- ###### スリーカードの判定
:::note
ファイブカード、フォーカード、フルハウスの場合もTrueを返却しますが、後の判定順序で重複カウントしないように制御します。
:::
```py
def isThreeOfAKind(hand):
    """スリーカードの判定
    手札にジョーカーがある場合
    ・残り４枚中２枚同じ数字があればTrue。
    手札にジョーカーがない場合
    ・５枚中３枚同じ数字があればTrue。
    """
    if hand[0]==JOKER:
        #ジョーカー以外の４枚中２枚同じ数字があればスリーカード
        return hand[1][1]==hand[2][1] or hand[2][1]==hand[3][1] or hand[3][1]==hand[4][1]
    else:
        #５枚中３枚同じ数字があればスリーカード
        return hand[0][1]==hand[1][1]==hand[2][1] or hand[1][1]==hand[2][1]==hand[3][1] or hand[2][1]==hand[3][1]==hand[4][1]
```
- - -
- ###### ツーペアの判定
:::note
ジョーカーがある場合、ツーペアが成立すれば上位のスリーカードも成立しますが、Trueを返却しておきます。後の判定順序で重複カウントしないように制御します。
:::
```py
def isTwoPair(hand):
    """ツーペアの判定
    手札にジョーカーがある場合
    ・残り４枚中２枚同じ数字があればTrue。（スリーカードと同じ判定条件）
    手札にジョーカーがない場合
    ・５枚中２枚同じ数字、残り３枚中２枚同じ数字ならばTrue。
    """
    if hand[0]==JOKER:
        #残り４枚中２枚同じ数字ならばツーペア（上位のスリーカードに吸収される）
        return hand[1][1]==hand[2][1] or hand[2][1]==hand[3][1] or hand[3][1]==hand[4][1]
    else:
        #同じ数字の組が２組あればツーペア
        return hand[0][1]==hand[1][1] and hand[2][1]==hand[3][1]\
            or hand[0][1]==hand[1][1] and hand[3][1]==hand[4][1]\
            or hand[1][1]==hand[2][1] and hand[3][1]==hand[4][1]
```
- - -
- ###### ワンペアの判定
:::note
上位の役の条件で既にTrueになっている場合もTrueを返却しますが、後の判定順序で重複カウントしないように制御します。
:::
```py
def isOnePair(hand):
    """ワンペアの判定
    手札にジョーカーがある場合
    ・無条件でTrue。
    手札にジョーカーがない場合
    ・５枚中２枚同じ数字があればTrue。
    """
    if hand[0]==JOKER:
        return True
    else:
        for i in range(1,len(hand)):
            if hand[i-1][1]==hand[i][1]:
                #数字の昇順に並んでいる前提なので、
                #隣り合う２枚の数字が同じならワンペア（以上）
                return True
        return False
```
- - -
- ###### 五重ループを再帰処理で実施
```py
def deal(hand, k):
    """カード配布＆判定"""
    global nPatterns
    global cntFiveOfAKind
    global cntRoyalStraightFlush
    global cntStraightFlush
    global cntFourOfAKind
    global cntFullHouse
    global cntFlush
    global cntStraight
    global cntThreeOfAKind
    global cntTwoPair
    global cntOnePair
    global cntNoPair

    if len(hand)<5:
        #手札が５枚になるまで配布
        for i in range(k,len(CARDS)):
            hand.append(CARDS[i])
            deal(hand,i+1)
            hand.pop()
    else:
        #手札が数字の昇順になっていることを確認
        for i in range(1,len(hand)):
            assert(hand[i-1][1]<=hand[i][1])

        #役判定（順番を間違えるとツーペアなのにワンペアと判定されたりします）
        nPatterns+=1
        if isFiveOfAKind(hand):
            cntFiveOfAKind+=1
        elif isRoyalStraightFlush(hand):
            cntRoyalStraightFlush+=1
        elif isStraightFlush(hand): 
            cntStraightFlush+=1
        elif isFourOfAKind(hand):
            cntFourOfAKind+=1
        elif isFullHouse(hand):
            cntFullHouse+=1
        elif isFlush(hand):
            cntFlush+=1
        elif isStraight(hand):
            cntStraight+=1
        elif isThreeOfAKind(hand):
            cntThreeOfAKind+=1
        elif isTwoPair(hand):
            cntTwoPair+=1
        elif isOnePair(hand):
            cntOnePair+=1
        else:
            cntNoPair+=1 
```
- - -
- ###### 出力形式
```py
def output(str, cnt):
    """役名(左寄せ21桁)、パターン数(右寄せ8桁)、確率(整数部3桁、小数部6桁)で出力
    """
    print(f"{str:<21}:{cnt:>8}({100*cnt/nPatterns:10.6f}%)")
```
- - -
- ###### メイン部分
`def`がたくさんあったので、`if __name__=="__main__":`で始めさせて頂きました。
```py
def main():
    deal([],0)
    output("Five of a Kind",cntFiveOfAKind)
    output("Royal Straight Flush",cntRoyalStraightFlush)
    output("Straight Flush",cntStraightFlush)
    output("Four of a Kind",cntFourOfAKind)
    output("Full House",cntFullHouse)
    output("Flush",cntFlush)
    output("Straight",cntStraight)
    output("Three of a Kind",cntThreeOfAKind)
    output("Two Pair",cntTwoPair)
    output("One Pair",cntOnePair)
    output("No Pair",cntNoPair)
    print('-'*len(f"{str():<21}:{int():>8}({float():10.6f}%)"))
    output("Total",nPatterns)

if __name__=="__main__":
    main()
```
- - -
- ###### 実行＆出力結果
```
python poker.py
```
```
Five of a Kind       :      13(  0.000453%)
Royal Straight Flush :      24(  0.000836%)
Straight Flush       :     180(  0.006272%)
Four of a Kind       :    3120(  0.108723%)
Full House           :    6552(  0.228318%)
Flush                :    7804(  0.271946%)
Straight             :   20532(  0.715479%)
Three of a Kind      :  137280(  4.783800%)
Two Pair             :  123552(  4.305420%)
One Pair             : 1268088( 44.189101%)
No Pair              : 1302540( 45.389651%)
-------------------------------------------
Total                : 2869685(100.000000%)
```
[次回](https://qiita.com/anonymously115/items/e170b164f9a2323dbe2b)、別の視点から検算する予定です。