kumanosawakouen
@kumanosawakouen (銀泉台)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

ポーカーのカード交換の最善手の統計を取るプログラムを作りました。

Q&A

Closed

pythonで一人用ポーカーのプログラムを作りました。
手札5枚を0枚から5枚交換する32通りのカードチェンジを行い、それぞれの役を判定。最も高い役をカウントして役の出現率の統計を取りました。ハイカードは役としてカウントしていません。
ちなみに10万回試行した場合の統計は次の通りでした。

追記
手札が2,3,4,5,7(スートバラバラ)、山に9,10,J,Q,Xとあった場合、xがどの値であっても
必ずストレート又はワンペアが成立します。なのでノーペアは出現しません。

3回目の変更でノーペアの出現を確認しました。前言撤回します。

ワンペア 12023
ツーペア 32377
スリーカード 35833
ストレート 9195
フラッシュ 5465
フルハウス 4341
フォーカード 718
ストレートフラッシュ 40
ロイヤルストレートフラッシュ 8
合計 100000

解決したいこと

何度かテストしましたが、もしバグや欠陥があればお知らせ下さい。

import itertools
import random
from collections import Counter

# ポーカーハンドのランク
RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
# スート
SUITS = ['', '', '', '']
# 役のランク
HAND_RANKS = ['ノーペア', 'ワンペア', 'ツーペア', 'スリーカード', 'ストレート', 'フラッシュ', 
              'フルハウス', 'フォーカード', 'ストレートフラッシュ', 'ロイヤルストレートフラッシュ']

# デッキを作成
deck = [rank + suit for rank in RANKS for suit in SUITS]

# 役の統計を保存する辞書
hand_statistics = Counter()

# 役判定関数
def judge(hand):
    ranks = sorted([card[:-1] for card in hand], key=RANKS.index)
    suits = [card[-1] for card in hand]
    rank_count = Counter(ranks)
    suit_count = Counter(suits)

    # ロイヤルストレートフラッシュ
    if ranks == RANKS[8:] and len(set(suits)) == 1:
        return "ロイヤルストレートフラッシュ"
    # ストレートフラッシュ
    elif len(set(suits)) == 1 and "".join(ranks) in "".join(RANKS):
        return "ストレートフラッシュ"
    # フォーカード
    elif 4 in rank_count.values():
        return "フォーカード"
    # フルハウス
    elif set(rank_count.values()) == {2, 3}:
        return "フルハウス"
    # フラッシュ
    elif len(set(suits)) == 1:
        return "フラッシュ"
    # ストレート
    elif "".join(ranks) in "".join(RANKS):
        return "ストレート"
    # スリーカード
    elif 3 in rank_count.values():
        return "スリーカード"
    # ツーペア
    elif list(rank_count.values()).count(2) == 2:
        return "ツーペア"
    # ワンペア
    elif 2 in rank_count.values():
        return "ワンペア"
    else:
        return "ノーペア"

# 1万回の試行
for _ in range(10000):
    # デッキからランダムに5枚のカードを選ぶ
    hand = random.sample(deck, 5)

    # 手札を0枚から5枚までチェンジする32通りの組み合わせを考える
    best_hand = hand.copy()
    best_rank = judge(hand)
    
    for i in range(6):
        for cards_to_change in itertools.combinations(range(5), i):
            new_hand = hand.copy()
            for j in cards_to_change:
                remaining_deck = [card for card in deck if card not in new_hand]
                new_hand[j] = random.choice(remaining_deck)
            new_rank = judge(new_hand)
            if HAND_RANKS.index(new_rank) > HAND_RANKS.index(best_rank):
                best_hand, best_rank = new_hand, new_rank

    hand_statistics[best_rank] += 1

print(hand_statistics)

コード変更1回目

ご指摘を受けて次のようにデバッグしました。
1,山から取ったカードは山から取り除く。(忘れてました)
2.A-2-3-4-5 の時もストレート、ストレートフラッシュ判定に加える。

import itertools
import random
from collections import Counter

# ポーカーハンドのランク
RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
# スート
SUITS = ['', '', '', '']
# 役のランク
HAND_RANKS = ['ノーペア', 'ワンペア', 'ツーペア', 'スリーカード', 'ストレート', 'フラッシュ', 
              'フルハウス', 'フォーカード', 'ストレートフラッシュ', 'ロイヤルストレートフラッシュ']

# デッキを作成
deck = [rank + suit for rank in RANKS for suit in SUITS]

# 役の統計を保存する辞書
hand_statistics = Counter()

# 役判定関数
def judge(hand):
    ranks = sorted([card[:-1] for card in hand], key=RANKS.index)
    suits = [card[-1] for card in hand]
    rank_count = Counter(ranks)
    suit_count = Counter(suits)

    # ロイヤルストレートフラッシュ
    if ranks == RANKS[8:] and len(set(suits)) == 1:
        return "ロイヤルストレートフラッシュ"
    # ストレートフラッシュ
    elif len(set(suits)) == 1 and ("".join(ranks) in "".join(RANKS) or ranks == RANKS[:4] + ['A']):
        return "ストレートフラッシュ"
    # フォーカード
    elif 4 in rank_count.values():
        return "フォーカード"
    # フルハウス
    elif set(rank_count.values()) == {2, 3}:
        return "フルハウス"
    # フラッシュ
    elif len(set(suits)) == 1:
        return "フラッシュ"
    # ストレート
    elif "".join(ranks) in "".join(RANKS) or ranks == RANKS[:4] + ['A']:
        return "ストレート"
    # スリーカード
    elif 3 in rank_count.values():
        return "スリーカード"
    # ツーペア
    elif list(rank_count.values()).count(2) == 2:
        return "ツーペア"
    # ワンペア
    elif 2 in rank_count.values():
        return "ワンペア"
    else:
        return "ノーペア"

# 1万回の試行
for _ in range(10000):
    # デッキからランダムに5枚のカードを選ぶ
    hand = random.sample(deck, 5)

    # 手札を0枚から5枚までチェンジする32通りの組み合わせを考える
    best_hand = hand.copy()
    best_rank = judge(hand)
    
    for i in range(6):
        for cards_to_change in itertools.combinations(range(5), i):
            new_hand = hand.copy()
            remaining_deck = [card for card in deck if card not in new_hand]
            for j in cards_to_change:
                new_card = random.choice(remaining_deck)
                new_hand[j] = new_card
                remaining_deck.remove(new_card)  # 選ばれたカードを山から除く
            new_rank = judge(new_hand)
            if HAND_RANKS.index(new_rank) > HAND_RANKS.index(best_rank):
                best_hand, best_rank = new_hand, new_rank

    hand_statistics[best_rank] += 1

print(hand_statistics)

改めて10万回試行しました。
image.png
ワンペア 11609
ツーペア 31548
スリーカード 35811
ストレート 10396
フラッシュ 5546
フルハウス 4275
フォーカード 757
ストレートフラッシュ 54
ロイヤルストレートフラッシュ 4
合計 100000

変更2回目

A-2-3-4-5の時も必ずストレート判定になるように変更しました。
ワンペアの判定を改善しました。

import itertools
import random
from collections import Counter

# ポーカーハンドのランク
RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
# スート
SUITS = ['', '', '', '']
# 役のランク
HAND_RANKS = ['ノーペア', 'ワンペア', 'ツーペア', 'スリーカード', 'ストレート', 'フラッシュ', 
              'フルハウス', 'フォーカード', 'ストレートフラッシュ', 'ロイヤルストレートフラッシュ']

# デッキを作成
deck = [rank + suit for rank in RANKS for suit in SUITS]

# 役の統計を保存する辞書
hand_statistics = Counter()

# 役判定関数
def judge(hand):
    ranks = sorted([card[:-1] for card in hand], key=RANKS.index)
    suits = [card[-1] for card in hand]
    rank_count = Counter(ranks)
    suit_count = Counter(suits)

    # ロイヤルストレートフラッシュ
    if ranks == RANKS[8:] and len(set(suits)) == 1:
        return "ロイヤルストレートフラッシュ"
    # ストレートフラッシュ
    elif len(set(suits)) == 1 and ("".join(ranks) in "".join(RANKS) or ranks == ['A', '2', '3', '4', '5']):
        return "ストレートフラッシュ"
    # フォーカード
    elif 4 in rank_count.values():
        return "フォーカード"
    # フルハウス
    elif set(rank_count.values()) == {2, 3}:
        return "フルハウス"
    # フラッシュ
    elif len(set(suits)) == 1:
        return "フラッシュ"
    # ストレート
    elif "".join(ranks) in "".join(RANKS) or ranks == ['A', '2', '3', '4', '5']:
        return "ストレート"
    # スリーカード
    elif 3 in rank_count.values():
        return "スリーカード"
    # ツーペア
    elif list(rank_count.values()).count(2) == 2:
        return "ツーペア"
    # ワンペア
    elif list(rank_count.values()).count(2) == 1:
        return "ワンペア"
    else:
        return "ノーペア"

# 1万回の試行
for _ in range(10000):
    # デッキからランダムに5枚のカードを選ぶ
    hand = random.sample(deck, 5)

    # 手札を0枚から5枚までチェンジする32通りの組み合わせを考える
    best_hand = hand.copy()
    best_rank = judge(hand)
    
    for i in range(6):
        for cards_to_change in itertools.combinations(range(5), i):
            new_hand = hand.copy()
            remaining_deck = [card for card in deck if card not in new_hand]
            for j in cards_to_change:
                new_card = random.choice(remaining_deck)
                new_hand[j] = new_card
                remaining_deck.remove(new_card)  # 選ばれたカードを山から除く
            new_rank = judge(new_hand)
            if HAND_RANKS.index(new_rank) > HAND_RANKS.index(best_rank):
                best_hand, best_rank = new_hand, new_rank

    hand_statistics[best_rank] += 1

print(hand_statistics)

コード変更3回目

1,必ず交換するカードは山の一番上から取っていく
2,1ゲーム終了後、新たに52枚のフルデッキを作り直す
3,ノーペアが出現します。

import itertools
import random
from collections import Counter

# ポーカーハンドのランク
RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
# スート
SUITS = ['', '', '', '']
# 役のランク
HAND_RANKS = ['ノーペア', 'ワンペア', 'ツーペア', 'スリーカード', 'ストレート', 'フラッシュ',
              'フルハウス', 'フォーカード', 'ストレートフラッシュ', 'ロイヤルストレートフラッシュ']

# 役判定関数
def judge(hand):
    ranks = sorted([card[:-1] for card in hand], key=RANKS.index)
    suits = [card[-1] for card in hand]
    rank_count = Counter(ranks)
    suit_count = Counter(suits)

    # ロイヤルストレートフラッシュ
    if ranks == RANKS[8:] and len(set(suits)) == 1:
        return "ロイヤルストレートフラッシュ"
    # ストレートフラッシュ
    elif len(set(suits)) == 1 and ("".join(ranks) in "".join(RANKS) or ranks == ['A', '2', '3', '4', '5']):
        return "ストレートフラッシュ"
    # フォーカード
    elif 4 in rank_count.values():
        return "フォーカード"
    # フルハウス
    elif set(rank_count.values()) == {2, 3}:
        return "フルハウス"
    # フラッシュ
    elif len(set(suits)) == 1:
        return "フラッシュ"
    # ストレート
    elif "".join(ranks) in "".join(RANKS) or ranks == ['A', '2', '3', '4', '5']:
        return "ストレート"
    # スリーカード
    elif 3 in rank_count.values():
        return "スリーカード"
    # ツーペア
    elif list(rank_count.values()).count(2) == 2:
        return "ツーペア"
    # ワンペア
    elif list(rank_count.values()).count(2) == 1:
        return "ワンペア"
    else:
        return "ノーペア"

# 役の統計を保存する辞書
hand_statistics = {hand_rank: 0 for hand_rank in HAND_RANKS}

# 1万回の試行
for _ in range(10000):
    # デッキを作成し、シャッフルする
    deck = [rank + suit for rank in RANKS for suit in SUITS]
    random.shuffle(deck)

    # デッキの上から5枚のカードを取る
    hand = [deck.pop() for _ in range(5)]

    # 手札を0枚から5枚までチェンジする32通りの組み合わせを考える
    best_hand = hand.copy()
    best_rank = judge(hand)

    for i in range(6):
        for cards_to_change in itertools.combinations(range(5), i):
            new_deck = deck.copy()  # デッキをコピー
            new_hand = hand.copy()
            for j in cards_to_change:
                new_hand[j] = new_deck.pop()  # 山から新しいカードを取る

            new_rank = judge(new_hand)
            if HAND_RANKS.index(new_rank) > HAND_RANKS.index(best_rank):
                best_hand, best_rank = new_hand, new_rank

    hand_statistics[best_rank] += 1

print(hand_statistics)

試行統計

10万回の試行統計は次の通りです。
image.png
ノーペア 2800
ワンペア 37137
ツーペア 34083
スリーカード 11663
ストレート 6391
フラッシュ 3974
フルハウス 3498
フォーカード 415
ストレートフラッシュ 34
ロイヤルストレートフラッシュ 5
合計 100000

0

2Answer

ワンペアの扱いですが、通常のポーカーであれば、2~Aでどの数字でも2枚でワンペアという役が成立しますが、ビデオポーカーの場合(ゲームの種類によって違いはあるようですが)、少なくとも10以上の数字(10,J,Q,K,A)のワンペアでなければ役としてカウントしないみたいです。
これを適用すると、ワンペアの出現率がフラッシュより低いことになる???

0Like

Comments

  1. なるほど、確かに。
    ではビデオポーカーじゃ無くて、一人用ポーカーということにします。
    ご指摘ありがとうございます。

  2. ワンペアの扱いは置いといて、

    冒頭の出現率の合計10万の中にノーペアが無いですが、毎回必ず何らかの役ができるということでしょうか? 

  3. はい。何度試行してもノーペアはできませんでした。必ず何かの役が発生しているようです。
    もっとも人間は山にあるカードを事前に知りようがないので、通常はノーペアができますが、最善手で交換した場合ノーペアはでませんでした。

  4. 確認ですが、
    1回につき、手札を0枚から5枚までチェンジする32通りは、どれも同じ山の上から0枚から5枚を取りますか? (実質、0枚は交換しませんが)
    山を毎回シャッフルしたら、条件が違ってくると思います。

  5. はい、その通りです。このコードでは、32通りのカードチェンジを同じ山(デッキ)から行っています。具体的には、remaining_deckは現在の手札を除いたデッキ(山)を表しています。そして、random.choice(remaining_deck)により、その山からランダムにカードを選んでいます。

    itertools.combinations(range(5), i)により、5枚のカードからi枚を選ぶすべての組み合わせを生成しています。そして、それぞれの組み合わせについて、選んだカードを山からランダムに選んだ新しいカードと交換しています。

    カード交換の時、山からカードを取り除くのを忘れてました。

    import itertools
    import random
    from collections import Counter
    
    # ポーカーハンドのランク
    RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
    # スート
    SUITS = ['', '', '', '']
    # 役のランク
    HAND_RANKS = ['ノーペア', 'ワンペア', 'ツーペア', 'スリーカード', 'ストレート', 'フラッシュ', 
                  'フルハウス', 'フォーカード', 'ストレートフラッシュ', 'ロイヤルストレートフラッシュ']
    
    # デッキを作成
    deck = [rank + suit for rank in RANKS for suit in SUITS]
    
    # 役の統計を保存する辞書
    hand_statistics = Counter()
    
    # 役判定関数
    def judge(hand):
        ranks = sorted([card[:-1] for card in hand], key=RANKS.index)
        suits = [card[-1] for card in hand]
        rank_count = Counter(ranks)
        suit_count = Counter(suits)
    
        # ロイヤルストレートフラッシュ
        if ranks == RANKS[8:] and len(set(suits)) == 1:
            return "ロイヤルストレートフラッシュ"
        # ストレートフラッシュ
        elif len(set(suits)) == 1 and "".join(ranks) in "".join(RANKS):
            return "ストレートフラッシュ"
        # フォーカード
        elif 4 in rank_count.values():
            return "フォーカード"
        # フルハウス
        elif set(rank_count.values()) == {2, 3}:
            return "フルハウス"
        # フラッシュ
        elif len(set(suits)) == 1:
            return "フラッシュ"
        # ストレート
        elif "".join(ranks) in "".join(RANKS):
            return "ストレート"
        # スリーカード
        elif 3 in rank_count.values():
            return "スリーカード"
        # ツーペア
        elif list(rank_count.values()).count(2) == 2:
            return "ツーペア"
        # ワンペア
        elif 2 in rank_count.values():
            return "ワンペア"
        else:
            return "ノーペア"
    
    # 1万回の試行
    for _ in range(10000):
        # デッキからランダムに5枚のカードを選ぶ
        hand = random.sample(deck, 5)
    
        # 手札を0枚から5枚までチェンジする32通りの組み合わせを考える
        best_hand = hand.copy()
        best_rank = judge(hand)
        
        for i in range(6):
            for cards_to_change in itertools.combinations(range(5), i):
                new_hand = hand.copy()
                remaining_deck = [card for card in deck if card not in new_hand]
                for j in cards_to_change:
                    new_card = random.choice(remaining_deck)
                    new_hand[j] = new_card
                    remaining_deck.remove(new_card)  # 選ばれたカードを山から除く
                new_rank = judge(new_hand)
                if HAND_RANKS.index(new_rank) > HAND_RANKS.index(best_rank):
                    best_hand, best_rank = new_hand, new_rank
    
        hand_statistics[best_rank] += 1
    
    print(hand_statistics)
    
  6. random.choice(remaining_deck)により、その山からランダムにカードを選んでいます。

    ランダムにカードを選んだ時点で公平で無い気がします。
    手札を0枚から5枚までチェンジする32通りを試すのであれば、山(カードの並び)は固定すべきでは無いかと思います。

    本来なら(?)、外側の(10万回の)ループの先頭で52枚のカードをシャッフルして山を固定。次に、その山の上から5枚を手札に配り、0枚から5枚までチェンジする32通りのループに入る。各ループでカード交換する0枚から5枚のカードは、常に(手札を配った残りの)山の上から順に取る。では無いかと思います。
    (リストの添え字操作だけなので、リストからカードを物理的に削除する必要が無い)

    これによって、出現率も微妙に差が出るのでは?(ノーペアが出るかも?)と思います。
    ぜひ、比較していただければと思います。

  7. おっしゃるとおりですね。必ず、山の一番上からカードを取るように改善しました。
    ご指摘ありがとうございます。

  8. 3,ノーペアが出現します。

    本当に出たんですね。
    逆にびっくり。なぜ出るようになったのか?
    数学が苦手で期待値計算できないので、検証できない・・・

    (独り言なので返信不要です)

  9. 例えば手札が2,3,4,5,7(スートバラバラ)
    山が8,9,10,Q,K の時は
    どうやってもノーペアになりますね。
    ノーペアの出現確率は3%ほどですが。

  10. どうやってもノーペアになりますね。

    たしかに。ありがとうございます。

    ビデオポーカーの場合、最善手が決まっているみたいなので(もちろん人間が取る手)、
    それをシミュレーションして勝ちパターンの統計を取るのも面白いかも。(私だけかも?)

    (独り言なので返信不要です)

一般的にストレート(およびストレートフラッシュ)は
A-2-3-4-5(最弱) ~ 10-J-Q-K-A(最強) という並びの定義が多いと思うのですが
最弱の方を除外しているのは意図的なものですか?

0Like

Comments

  1. ご指摘ありがとうございます。A-2-3-4-5のときもストレート、ストレートフラッシュが成立するように役判定関数の部分を改良しました。またK-A-2-3-4 などは、ストレートとして成立しないようにしました。

    # 役判定関数
    def judge(hand):
        ranks = sorted([card[:-1] for card in hand], key=RANKS.index)
        suits = [card[-1] for card in hand]
        rank_count = Counter(ranks)
        suit_count = Counter(suits)
    
        # ロイヤルストレートフラッシュ
        if ranks == RANKS[8:] and len(set(suits)) == 1:
            return "ロイヤルストレートフラッシュ"
        # ストレートフラッシュ
        elif len(set(suits)) == 1 and ("".join(ranks) in "".join(RANKS) or ranks == RANKS[:4] + ['A']):
            return "ストレートフラッシュ"
        # フォーカード
        elif 4 in rank_count.values():
            return "フォーカード"
        # フルハウス
        elif set(rank_count.values()) == {2, 3}:
            return "フルハウス"
        # フラッシュ
        elif len(set(suits)) == 1:
            return "フラッシュ"
        # ストレート
        elif "".join(ranks) in "".join(RANKS) or ranks == RANKS[:4] + ['A']:
            return "ストレート"
        # スリーカード
        elif 3 in rank_count.values():
            return "スリーカード"
        # ツーペア
        elif list(rank_count.values()).count(2) == 2:
            return "ツーペア"
        # ワンペア
        elif 2 in rank_count.values():
            return "ワンペア"
        else:
            return "ノーペア"
    

Your answer might help someone💌