ポーカーをコンピュータと対戦できるプログラムを書いてみた

私は現在、ポーカーと眼球運動に関する研究をしています。その研究で必要な実験環境を整えるために、自分でポーカープログラムを開発しました。

# 仕様
* 基本的にはテキサスホールデムポーカーのルール
* コンピュータとプレイヤーの1対1による対戦
* ベットは常にコンピュータが先
* コンピュータ自身が見えていないカードを乱数で決定し、モンテカルロシミュレーションによって計算した勝率によって、コンピュータは行動選択を行う。
* 使用言語はPython

# コード
```python:porker_play.py
# ポーカーのデッキ作成やラウンド進行、結果発表を行うプログラムです。

import random
from odds_calculate import computer_handler
from determine_winner import determine_winner
from player_option import first_option, option_to_reraise, option_to_allin

suits = ['♠', '♣', '♢', '♡']
ranks = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A']


class Card:
    def __init__(self, suit, rank):
        self.suit = suit
        self.rank = rank

    def __str__(self):
        return f'{self.rank}{self.suit}'


def create_deck():
    deck = [Card(suit, rank) for suit in suits for rank in ranks]
    random.shuffle(deck)
    return deck


def deal_hands(deck):
    player_hand = [deck.pop(), deck.pop()]
    computer_hand = [deck.pop(), deck.pop()]
    return player_hand, computer_hand


def deal_community_cards(deck):
    return [deck.pop() for _ in range(5)]


def show_hands(player_hand, community_cards, revealed, computer_hand=None):
    print(f"あなたの手札: {', '.join(str(card) for card in player_hand)}")
    print(f"コミュニティカード: {', '.join(str(card) if i < revealed else 'X' for i, card in enumerate(community_cards))}")
    if computer_hand:
        print(f"コンピュータの手札: {', '.join(str(card) for card in computer_hand)}")
    else:
        print("コンピュータの手札: 非公開")


def betting_round(player_chips, computer_chips, pot, game_over, folder):
    # コンピュータのベット
    # プリフロップの場合
    if pot == 0:
        bet_amount = 10
        print(f"コンピュータは{bet_amount}チップをベットしました。")
        computer_behavior = 10
    # フロップ以降
    else:
        computer_behavior = computer_handler(False, 0, 0, computer_chips, pot, computer_hand_str, community_cards_str)
        if computer_behavior == "チェック":
            bet_amount = 0
            print("コンピュータはチェックしました。")
        elif computer_behavior == "オールイン":
            bet_amount = computer_chips
            print("コンピュータはオールインしました。")
        else:
            bet_amount = computer_behavior
            print(f"コンピュータは{bet_amount}チップをベットしました。")
    computer_chips -= bet_amount
    pot += bet_amount
    print(f"現在のポット: {pot}チップ")

    # レイズ変数の設定
    raised = False
    # プレイヤーの行動選択の入力
    player_bet = first_option(computer_behavior, bet_amount, player_chips, pot)

    # プレイヤーがフォールドした場合
    if player_bet == 'f':
        computer_chips += pot
        pot = 0
        game_over = True
        folder = "player"
        print("あなたがフォールドしました。コンピュータが勝ちました。")
        return player_chips, computer_chips, pot, game_over, folder
    
    # プレイヤーがオールインした場合
    elif player_bet == 'a':
        pot += player_chips
        player_chips = 0
        current_bet = pot - bet_amount
        print("あなたはオールインしました。")
        # コンピュータの行動選択
        computer_behavior = computer_handler(True, current_bet, current_bet - bet_amount, computer_chips, pot, computer_hand_str, community_cards_str)
        if computer_behavior != "フォールド":
            computer_behavior = "オールイン"
        if computer_behavior == "フォールド":
            player_chips += pot
            pot = 0
            game_over = True
            folder = "computer"
            print("コンピュータがフォールドしました。あなたの勝ちです。")
            return player_chips, computer_chips, pot, game_over, folder
        # オールインの場合
        else:
            pot += computer_chips
            computer_chips = 0
            game_over = True
            print("コンピュータはオールインしました。")
            return player_chips, computer_chips, pot, game_over, folder

    # プレイヤーがコールした場合
    elif player_bet == 'c':
        player_chips -= bet_amount
        pot += bet_amount
        print("あなたはコールしました。次のラウンドに進みます。")
        return player_chips, computer_chips, pot, game_over, folder

    # プレイヤーがレイズした場合
    else:
        player_chips -= player_bet
        pot += player_bet
        print(f"現在のポット: {pot}チップ")
        raise_amount = player_bet - bet_amount
        raised = True
        # コンピュータの行動選択
        while raised:
            computer_behavior = computer_handler(True, player_bet, raise_amount, computer_chips, pot, computer_hand_str, community_cards_str)

            if computer_behavior == "フォールド":
                player_chips += pot
                pot = 0
                game_over = True
                folder = "computer"
                print("コンピュータがフォールドしました。あなたの勝ちです。")
                break
            elif computer_behavior == "コール":
                computer_chips -= raise_amount
                pot += raise_amount
                print("コンピュータはコールしました。")
                break
            elif computer_behavior == "オールイン":
                pot += computer_chips
                computer_chips = 0
                print("コンピュータはオールインしました。")

                # プレイヤーの行動選択の入力
                player_bet = option_to_allin()
                if player_bet == 'a':
                    pot += player_chips
                    player_chips = 0
                    game_over = True
                    print("あなたはオールインしました。")
                    break
                # フォールドの場合
                else:
                    computer_chips += pot
                    pot = 0
                    game_over = True
                    folder = "player"
                    print("あなたがフォールドしました。コンピュータの勝ちです。")
                    break

            # リレイズの処理
            else:
                new_bet = computer_behavior
                computer_chips -= new_bet - bet_amount
                pot += new_bet - bet_amount
                bet_amount = new_bet
                print(f"コンピュータは{new_bet}チップにリレイズしました。")
                # プレイヤーの行動選択の入力
                raised_player_bet = option_to_reraise(new_bet, player_bet, player_chips, False)
                while True:
                    try:
                        if raised_player_bet == 'a':
                            pot += player_chips
                            current_bet = player_bet + player_chips
                            player_chips = 0
                            print("あなたがオールインしました。")
                            # コンピュータの行動選択
                            computer_behavior = computer_handler(True, current_bet, current_bet - new_bet, computer_chips, pot, computer_hand_str, community_cards_str)
                            if computer_behavior != "フォールド":
                                computer_behavior = "オールイン"

                            if computer_behavior == "フォールド":
                                player_chips += pot
                                pot = 0
                                game_over = True
                                folder = "computer"
                                print("コンピュータがフォールドしました。あなたの勝ちです。")
                            # オールインの場合
                            else:
                                pot += computer_chips
                                computer_chips = 0
                                game_over = True
                                print("コンピュータはオールインしました。")
                            break

                        elif raised_player_bet == 'f':
                            game_over = True
                            folder = "player"
                            computer_chips += pot
                            pot = 0
                            print("あなたがフォールドしました。コンピュータが勝ちました。")
                            break

                        elif raised_player_bet == 'c':
                            player_chips -= new_bet - player_bet
                            pot += new_bet - player_bet
                            raised = False
                            print("あなたがコールしました")
                            break

                        # レイズの場合
                        else:
                            raised_player_bet = int(raised_player_bet)
                            if raised_player_bet < new_bet * 2 or raised_player_bet > player_bet + player_chips - 1:
                                raise ValueError
                            else:
                                player_chips -= raised_player_bet - player_bet
                                pot += raised_player_bet - player_bet
                                player_bet = raised_player_bet
                                break
                    except ValueError:
                        raised_player_bet = option_to_reraise(new_bet, player_bet, player_chips, True)

    return player_chips, computer_chips, pot, game_over, folder


def main():
    # 初期設定
    player_chips = 1000
    computer_chips = 1000
    pot = 0
    game_over = False
    folder = "None"
    deck = create_deck()

    # グローバル変数の宣言
    global player_hand, computer_hand, community_cards, revealed
    player_hand, computer_hand = deal_hands(deck)
    community_cards = deal_community_cards(deck)
    global player_hand_str, computer_hand_str, community_cards_str
    player_hand_str = [str(card) for card in player_hand]
    computer_hand_str = [str(card) for card in computer_hand]
    full_community_cards_str = [str(card) for card in community_cards]

    # 各ラウンドの実施
    print("\nプリフロップベッティングラウンド")
    revealed = 0
    community_cards_str = full_community_cards_str[0:revealed]
    show_hands(player_hand, community_cards, revealed)
    player_chips, computer_chips, pot, game_over, folder = betting_round(player_chips, computer_chips, pot, game_over, folder)
    # game_over が False の場合のみ次のラウンドを実行
    if not game_over:
        print("\nフロップ")
        revealed = 3
        community_cards_str = full_community_cards_str[0:revealed]
        show_hands(player_hand, community_cards, revealed)
        player_chips, computer_chips, pot, game_over, folder = betting_round(player_chips, computer_chips, pot, game_over, folder)

    if not game_over:
        print("\nターン")
        revealed = 4
        community_cards_str = full_community_cards_str[0:revealed]
        show_hands(player_hand, community_cards, revealed)
        player_chips, computer_chips, pot, game_over, folder = betting_round(player_chips, computer_chips, pot, game_over, folder)

    if not game_over:
        print("\nリバー")
        revealed = 5
        community_cards_str = full_community_cards_str[0:revealed]
        show_hands(player_hand, community_cards, revealed)
        player_chips, computer_chips, pot, game_over, folder = betting_round(player_chips, computer_chips, pot, game_over, folder)

    if folder == "None":
        print("\nショーダウン")
        revealed = 5
        show_hands(player_hand, community_cards, revealed, computer_hand)
    else:
        print("\nショーダウンは行われませんでした。")

    # 勝者判定
    if folder == "None":
        result = determine_winner(player_hand_str, computer_hand_str, full_community_cards_str)
        print(result)
        if result == "プレイヤーの勝ち":
            player_chips += pot
            pot = 0
        elif result == "コンピュータの勝ち":
            computer_chips += pot
            pot = 0
        else:
            player_chips += int(pot / 2)
            computer_chips += int(pot / 2)
            pot = 0
    
    # 結果発表
    print(f"最終結果: プレイヤー {player_chips}チップ, コンピュータ {computer_chips}チップ")

if __name__ == "__main__":
    main()
```

```python:odds_calculate.py
# コンピュータが行動選択を行うためのプログラムです。

import random
from collections import Counter
from itertools import combinations

# コンピュータの希望追加ベット額を期待値から求める関数
def simulate_holdem(hole_cards, community_cards, current_bet, raised_chip, pot, num_opponents=1, num_trials=3000):
    deck = [r + s for r in "23456789TJQKA" for s in "♠♣♢♡" if r + s not in hole_cards + community_cards]

    def hand_strength(hand):
        hand = sorted(hand, key=lambda c: "23456789TJQKA".index(c[0]), reverse=True)
        counts = Counter([card[0] for card in hand])
        count_vals = list(reversed(sorted(counts.values())))
        groups = sorted(list(counts.items()), key=lambda c: (c[1], "23456789TJQKA".index(c[0])), reverse=True)

        if len(set([card[1] for card in hand])) == 1 and "AKQJT" in "".join([card[0] for card in hand]):
            return (9, ) + tuple("23456789TJQKA".index(r) for r, _ in groups)
        elif len(set([card[1] for card in hand])) == 1 and "".join([card[0] for card in hand]) in "AKQJT98765432":
            return (8, ) + tuple("23456789TJQKA".index(r) for r, _ in groups)
        elif count_vals == [4, 1]:
            primary, kicker = groups[0][0], groups[1][0]
            return (7, "23456789TJQKA".index(primary), "23456789TJQKA".index(kicker))
        elif count_vals == [3, 2]:
            primary, secondary = groups[0][0], groups[1][0]
            return (6, "23456789TJQKA".index(primary), "23456789TJQKA".index(secondary))
        elif len(set([card[1] for card in hand])) == 1:
            return (5, ) + tuple("23456789TJQKA".index(r) for r, _ in groups)
        elif "".join([card[0] for card in hand]) in "AKQJT98765432":
            return (4, ) + tuple("23456789TJQKA".index(r) for r, _ in groups)
        elif "A5432" in "".join([card[0] for card in hand]):# Aを最低ランクとして使える場合の例外処理
            return (4, 3, 2, 1, 0, -1)
        elif count_vals == [3, 1, 1]:
            primary, kicker1, kicker2 = groups[0][0], groups[1][0], groups[2][0]
            return (3, "23456789TJQKA".index(primary), "23456789TJQKA".index(kicker1), "23456789TJQKA".index(kicker2))
        elif count_vals == [2, 2, 1]:
            primary, secondary, kicker = groups[0][0], groups[1][0], groups[2][0]
            return (2, "23456789TJQKA".index(primary), "23456789TJQKA".index(secondary), "23456789TJQKA".index(kicker))
        elif count_vals == [2, 1, 1, 1]:
            primary, kicker1, kicker2, kicker3 = groups[0][0], groups[1][0], groups[2][0], groups[3][0]
            return (1, "23456789TJQKA".index(primary), "23456789TJQKA".index(kicker1), "23456789TJQKA".index(kicker2), "23456789TJQKA".index(kicker3))
        else:
            return (0, ) + tuple("23456789TJQKA".index(r) for r, _ in groups)

    def best_hand(cards):
        return max(combinations(cards, 5), key=hand_strength)

    def trial():
        random.shuffle(deck)
        my_cards = hole_cards + community_cards + deck[:5 - len(community_cards)]
        my_best_hand = best_hand(my_cards)
        opponent_hands = [deck[i:i+2] for i in range(5 - len(community_cards), 5 - len(community_cards) + 2 * num_opponents, 2)]
        opponent_best_hands = [best_hand(opponent_hand + community_cards + deck[:5 - len(community_cards)]) for opponent_hand in opponent_hands]

        num_better_hands = sum(1 for opponent_best_hand in opponent_best_hands if hand_strength(opponent_best_hand) > hand_strength(my_best_hand))
        return num_better_hands == 0

    num_wins = sum(1 for _ in range(num_trials) if trial())
    odds = num_wins / num_trials
    # コンピュータの確率表示
    # print(str(odds * 100) + "%")
    desired_bet = int(odds * pot / (1 - odds)) - (current_bet - raised_chip)
    return desired_bet

# 希望追加ベット額と場の状況からコンピュータの意思決定を行う関数
def computer_handler(raised, current_bet, raised_chip, computer_chips, pot, hole_cards, community_cards):
    # 最初のベットの場合
    if not raised:
        desired_bet = simulate_holdem(hole_cards, community_cards, 0, 0, pot)
        if desired_bet < pot / 2:
            behavior = "チェック"
        elif desired_bet >= pot / 2 and desired_bet < computer_chips:
            behavior = desired_bet
        # 希望追加ベット額が所持チップよりも大きい場合
        else:
            behavior = "オールイン"

    # プレイヤーのレイズ以降
    if raised:
        desired_bet = simulate_holdem(hole_cards, community_cards, current_bet, raised_chip, pot)
        if desired_bet < raised_chip:
            behavior = "フォールド"
        elif desired_bet >= raised_chip and desired_bet < current_bet + raised_chip:
            behavior = "コール"
        elif desired_bet >= current_bet + raised_chip and desired_bet < computer_chips:
            behavior = desired_bet
        # 希望ベット額が所持チップよりも大きい場合
        else:
            behavior = "オールイン"

    return behavior
```

```python:player_option.py
# プレイヤーに行動選択の入力を求めるプログラムです。

import math

# 各ラウンドで最初にプレイヤーが行動選択を入力する関数
def first_option(computer_behavior, bet_amount, player_chips, pot):
    if computer_behavior == "チェック":
        player_bet = input(f"あなたのターン:チェックする場合は 'c', オールインする場合は 'a', ベットする場合は金額({math.ceil(pot / 2)}-{player_chips})を入力: ")
    elif bet_amount * 2 < player_chips:
        player_bet = input(f"あなたのターン:コールする場合は 'c', フォールドする場合は 'f', オールインする場合は 'a', レイズする場合は金額({bet_amount * 2}-{player_chips - 1})を入力: ")
    else:
        player_bet = input("あなたのターン:コールする場合は 'c', フォールドする場合は 'f', オールインする場合は 'a'を入力: ")

    while True:
        try:
            if player_bet in ['c', 'f', 'a']:
                break
            # レイズの場合
            else:
                player_bet = int(player_bet)
                if player_bet < bet_amount * 2 or player_bet > player_chips - 1:
                    raise ValueError
            break
        except ValueError:
            if computer_behavior == "チェック":
                player_bet = input(f"無効な入力です:チェックする場合は 'c', オールインする場合は 'a', ベットする場合は金額({math.ceil(pot / 2)}-{player_chips})を入力: ")
            elif bet_amount * 2 < player_chips:
                player_bet = input(f"無効な入力です:コールする場合は 'c', フォールドする場合は 'f', オールインする場合は 'a', レイズする場合は金額({bet_amount * 2}-{player_chips - 1})を入力: ")
            else:
                player_bet = input("無効な入力です:コールする場合は 'c', フォールドする場合は 'f', オールインする場合は 'a'を入力: ")
    return player_bet


# リレイズに対してプレイヤーが行動選択を入力する関数
def option_to_reraise(new_bet, player_bet, player_chips, errored):
    if not errored:
        if new_bet * 2 < player_bet + player_chips:
            raised_player_bet = input(f"あなたのターン:コールする場合は 'c', フォールドする場合は 'f', オールインする場合は 'a', レイズする場合は金額({new_bet * 2}-{player_bet + player_chips - 1})を入力: ")
        else:
            raised_player_bet = input("あなたのターン:コールする場合は 'c', フォールドする場合は 'f', オールインする場合は 'a'を入力: ")
    else:
        if new_bet * 2 < player_bet + player_chips:
            raised_player_bet = input(f"無効な入力です:コールする場合は 'c', フォールドする場合は 'f', オールインする場合は 'a', レイズする場合は金額({new_bet * 2}-{player_bet + player_chips - 1})を入力: ")
        else:
            raised_player_bet = input("無効な入力です:コールする場合は 'c', フォールドする場合は 'f', オールインする場合は 'a'を入力: ")

    return raised_player_bet


# オールインに対してプレイヤーが行動選択を入力する関数
def option_to_allin():
    player_bet = input("あなたのターン:フォールドする場合は 'f', オールインする場合は 'a': ")

    # 例外処理
    while True:
        if player_bet not in ['f', 'a']:
            player_bet = input("無効な入力です:フォールドする場合は 'f', オールインする場合は 'a': ")
        else:
            break
    return player_bet
```

```python:determine_winner.py
# 勝者を判定するプログラムです。

from itertools import combinations
from collections import Counter

# 役・キッカーから手札の強さを評価する関数
from collections import Counter

def hand_strength(hand):
    hand = sorted(hand, key=lambda c: "23456789TJQKA".index(c[0]), reverse=True)
    counts = Counter([card[0] for card in hand])
    count_vals = list(reversed(sorted(counts.values())))
    groups = sorted(list(counts.items()), key=lambda c: (c[1], "23456789TJQKA".index(c[0])), reverse=True)

    if len(set([card[1] for card in hand])) == 1 and "AKQJT" in "".join([card[0] for card in hand]):
        return (9, ) + tuple("23456789TJQKA".index(r) for r, _ in groups), "ロイヤルストレートフラッシュ"
    elif len(set([card[1] for card in hand])) == 1 and "".join([card[0] for card in hand]) in "AKQJT98765432":
        return (8, ) + tuple("23456789TJQKA".index(r) for r, _ in groups), "ストレートフラッシュ"
    elif count_vals == [4, 1]:
        primary, kicker = groups[0][0], groups[1][0]
        return (7, "23456789TJQKA".index(primary), "23456789TJQKA".index(kicker)), "フォーカード"
    elif count_vals == [3, 2]:
        primary, secondary = groups[0][0], groups[1][0]
        return (6, "23456789TJQKA".index(primary), "23456789TJQKA".index(secondary)), "フルハウス"
    elif len(set([card[1] for card in hand])) == 1:
        return (5, ) + tuple("23456789TJQKA".index(r) for r, _ in groups), "フラッシュ"
    elif "".join([card[0] for card in hand]) in "AKQJT98765432":
        return (4, ) + tuple("23456789TJQKA".index(r) for r, _ in groups), "ストレート"
    elif "A5432" in "".join([card[0] for card in hand]):# Aを最低ランクとして使える場合の例外処理
        return (4, 3, 2, 1, 0, -1), "ストレート"
    elif count_vals == [3, 1, 1]:
        primary, kicker1, kicker2 = groups[0][0], groups[1][0], groups[2][0]
        return (3, "23456789TJQKA".index(primary), "23456789TJQKA".index(kicker1), "23456789TJQKA".index(kicker2)), "スリーカード"
    elif count_vals == [2, 2, 1]:
        primary, secondary, kicker = groups[0][0], groups[1][0], groups[2][0]
        return (2, "23456789TJQKA".index(primary), "23456789TJQKA".index(secondary), "23456789TJQKA".index(kicker)), "ツーペア"
    elif count_vals == [2, 1, 1, 1]:
        primary, kicker1, kicker2, kicker3 = groups[0][0], groups[1][0], groups[2][0], groups[3][0]
        return (1, "23456789TJQKA".index(primary), "23456789TJQKA".index(kicker1), "23456789TJQKA".index(kicker2), "23456789TJQKA".index(kicker3)), "ワンペア"
    else:
        return (0, ) + tuple("23456789TJQKA".index(r) for r, _ in groups), "ハイカード"


# 最も強い手札を選ぶ関数
def best_hand(cards):
    return max(combinations(cards, 5), key=lambda hand: hand_strength(hand)[0])

# 出力時のフォーマット修正を行う関数
def format_hand(hand):
    hand_str = ", ".join([card[0] + card[1] for card in hand])
    return "({})".format(hand_str)

# 勝敗判定を行い、役名と最強の５枚のカードを表示する関数
def determine_winner(player_hand, computer_hand, community_cards):
    player_best_hand = best_hand(player_hand + community_cards)
    computer_best_hand = best_hand(computer_hand + community_cards)

    player_strength, player_hand_name = hand_strength(player_best_hand)
    computer_strength, computer_hand_name = hand_strength(computer_best_hand)

    result = ""
    if player_strength > computer_strength:
        result = "プレイヤーの勝ち"
    elif player_strength < computer_strength:
        result = "コンピュータの勝ち"
    else:
        result = "引き分け"

    print("プレイヤーの手札: {} 役: {}".format(format_hand(player_best_hand), player_hand_name))
    print("コンピュータの手札: {} 役: {}".format(format_hand(computer_best_hand), computer_hand_name))

    return result
```

長くなりましたが、以上の関数を用いてポーカーを遊ぶことが出来ます。
モンテカルロシミュレーションを用いているので、コンピュータの行動選択には数秒かかります。
ブラフ等をすることはないので、そういった点を変更する場合は更に変数を加える必要があるでしょう。

コンピュータとポーカーを練習したい人、他のカードゲームに応用してみたい人は是非このコードで遊んでみてください！