乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

テキサスホールデム

今回はオンカジに有りそうな
ポーカーBOTについてです。


解説動画はこちら


 

この内容はあくまでも
BOTプログラムに関する内容となります。




プログラムの内容


ポーカーのテキサスホールデムルールのゲームを行える
Streamlitプログラムです。

BOT用のストラテジークラスがあり
それを改変することでBOTの戦略を
構築することができるようになっています。




テキサスホールデムのルール

自分だけの手札2枚と場に共有される5枚のカード
(コミュニティカード)を組み合わせて
最も強い5枚の役を作るポーカーゲーム

ゲームはブラインドベットで開始され
プリフロップ、フロップ、ターン、リバーの
4つのラウンドでそれぞれベットが行われる

各ラウンドでは
コール、レイズ、フォールド、チェックなどのアクションを選択し
最終的に残ったプレイヤーの中で
最も強い役を持っていたプレイヤーがポットを獲得



今回のBOTの特徴

プリフロップはそれなり
ポストフロップは運任せ
という戦略です。

細かいルールは

ハンド強度評価を行い
3段階の強度分類でアクションを決めます。

評価基準(スコア10-50程度)

ペア: +30 + ハイカードランク
AA = 30 + 14 = 44点(最強)
KK = 30 + 13 = 43点
22 = 30 + 2 = 32点

スーテッド: +5点
同じスーツなら追加ボーナス

ハイカード: high/2 + low/5
AK = 14/2 + 13/5 = 7 + 2.6 = 9.6点
スーテッドAK = 9.6 + 5 = 14.6点


強度分類

強いハンド (45点以上)
主にAA, KK, QQ相当
積極的にレイズ/ベット
チップが少なければオールイン

中程度のハンド (35-44点)
中位ペア、AK、強いスーテッドコネクター
チェック/コール中心
守備的プレイ

弱いハンド (34点以下)
低ペア、アンスーテッドローカード
チェックできればチェック
ベットがあればフォールド


こんな戦略をとるBOTです。


プログラムの詳細

3つのプログラムからなっています。
main.py : StreamlitのUIなど
poker_engine.py : ポーカーゲームの制御
bot_strategy.py : BOTのストラテジー

ローカルでStreamlitを動かす場合はインストールが必要です。
pip install streamlit


main.py
import streamlit as st
from poker_engine import PokerGame
from bot_strategy import BotStrategy

def init_game():
    """ゲーム初期化"""
    player_names = ["You"] + [f"BOT{i+1}" for i in range(7)]
    st.session_state.game = PokerGame(player_names)
    st.session_state.game.new_hand()
    st.session_state.pending_action = None

def process_pending():
    """保留中のアクションを処理"""
    if st.session_state.pending_action is not None:
        idx, action = st.session_state.pending_action
        st.session_state.pending_action = None
        st.session_state.game.apply_action(idx, action)

def auto_advance_bots_until_user_or_showdown(max_steps=50):
    """Botの連続ターンを自動進行"""
    game = st.session_state.game
    steps = 0
    
    while steps < max_steps:
        if game.phase == "showdown":
            return
        
        current_player = game.get_current_player()
        if current_player is None:
            return
        
        if current_player.name == "You" or not current_player.can_act():
            return
        
        # Bot行動
        bot = BotStrategy(current_player.name)
        action = bot.decide_action(
            current_player.hand, 
            game.community, 
            current_player.chips,
            game.current_bet, 
            current_player.bet_this_round
        )
        
        game.apply_action(game.current_index, action)
        steps += 1

def draw_player_panel():
    """プレイヤー情報パネルを描画"""
    game = st.session_state.game
    cols = st.columns(8)
    
    for i, player in enumerate(game.players):
        with cols[i]:
            # プレイヤー名の装飾
            name = player.name
            if player.allin:
                name += " (ALL-IN)"
            if i == game.dealer_index:
                name += " [D]"
            if i == game.sb_index:
                name += " [SB]"
            if i == game.bb_index:
                name += " [BB]"
            
            # 非アクティブ・フォールドプレイヤーは取り消し線
            if not player.active or player.folded:
                st.markdown(f"~~**{name}**~~")
            else:
                st.markdown(f"**{name}**")
            
            # チップ表示
            st.write(f"Chips: {player.chips}")
            
            # ベット額表示
            if player.bet_this_round > 0:
                st.write(f"Bet: {player.bet_this_round}")
            
            # あなたの手札のみ表示
            if player.name == "You" and player.hand:
                st.write(f"Hand: {player.hand[0]}  {player.hand[1]}")

def draw_community_area():
    """コミュニティエリアを描画"""
    game = st.session_state.game
    
    st.subheader("Dealer Zone")
    
    # コミュニティカード表示
    if game.community:
        community_str = " ".join(game.community)
        st.write(f"Community: {community_str}")
    else:
        st.write("Community: (No cards yet)")
    
    # ポット表示
    st.write(f"Pot: {game.pot}")
    
    # ゲーム状況メッセージ
    st.info(game.message)

def draw_action_interface():
    """アクションインターフェースを描画"""
    game = st.session_state.game
    
    if game.phase == "showdown":
        st.success("Hand complete. Click 'New Hand' to continue.")
        if game.game_over:
            st.warning("Game over. Your chips are gone or opponents busted. Click 'New Hand' to restart.")
        return
    
    current_player = game.get_current_player()
    if current_player is None or current_player.name != "You":
        # あなたのターンではない
        if current_player:
            st.info(f"Waiting for {current_player.name} to act...")
        return
    
    if not current_player.can_act():
        return
    
    # アクション選択UI
    st.subheader("Your Turn")
    
    legal_actions = game.legal_actions(game.current_index)
    to_call = max(0, game.current_bet - current_player.bet_this_round)
    
    # アクションラベルを整形
    action_labels = []
    for action in legal_actions:
        if action == "call":
            action_labels.append(f"Call ({to_call})")
        elif action == "bet":
            action_labels.append("Bet (10)")
        elif action == "raise":
            action_labels.append("Raise (+10)")
        else:
            action_labels.append(action.capitalize())
    
    # ラジオボタンでアクション選択
    selected_action = st.radio(
        "Choose your action:",
        legal_actions,
        format_func=lambda x: action_labels[legal_actions.index(x)],
        horizontal=True,
        key="player_action"
    )
    
    # アクション実行ボタン
    if st.button("Submit Action", type="primary"):
        st.session_state.pending_action = (game.current_index, selected_action)
        st.rerun()

def main():
    st.set_page_config(
        page_title="Texas Hold'em Poker",
        page_icon="♠",
        layout="wide"
    )
    
    st.title("♠ Texas Hold'em Poker")
    st.markdown("---")
    
    # ゲーム初期化
    if "game" not in st.session_state:
        init_game()
    
    # 上部コントロール
    col1, col2, col3 = st.columns([1, 1, 2])
    
    with col1:
        if st.button("New Hand", type="secondary"):
            st.session_state.game.new_hand()
            st.rerun()
    
    with col2:
        if st.button("Restart Game", type="secondary"):
            init_game()
            st.rerun()
    
    with col3:
        game = st.session_state.game
        st.write(f"Phase: **{game.phase.title()}** | Current Bet: **{game.current_bet}**")
    
    st.markdown("---")
    
    # 保留中のアクションを処理
    process_pending()
    
    # Botの自動進行
    auto_advance_bots_until_user_or_showdown()
    
    # プレイヤー情報表示
    st.subheader("Players")
    draw_player_panel()
    
    st.markdown("---")
    
    # コミュニティエリア表示
    draw_community_area()
    
    st.markdown("---")
    
    # アクションインターフェース
    draw_action_interface()

if __name__ == "__main__":
    main()






poker_engine.py
import random
import itertools
from typing import List, Dict, Tuple, Optional

# ========= 基本定義 =========
SUITS = ["♠", "♥", "♦", "♣"]
RANKS = ["2","3","4","5","6","7","8","9","T","J","Q","K","A"]
RANK_VAL = {r:i for i,r in enumerate(RANKS, start=2)}
INITIAL_CHIPS = 1000

def create_deck() -> List[str]:
    """52枚のデッキを作成"""
    return [r+s for r,s in itertools.product(RANKS, SUITS)]

# ========= ハンド評価 =========
def hand_rank(cards5: List[str]) -> Tuple:
    """
    5枚のカードから役の強さを評価
    返り値: (rank_class, high cards...) で大きい方が強い
    rank_class: 8=ストレートフラッシュ, 7=フォーカード, 6=フルハウス,
               5=フラッシュ, 4=ストレート, 3=スリーカード, 2=ツーペア, 1=ワンペア, 0=ハイカード
    """
    vals = sorted([RANK_VAL[c[0]] for c in cards5], reverse=True)
    suits = [c[1] for c in cards5]
    counts = {}
    for v in vals:
        counts[v] = counts.get(v, 0) + 1
    by_count = sorted(counts.items(), key=lambda x:(x[1], x[0]), reverse=True)
    is_flush = len(set(suits)) == 1
    
    # ストレート判定(Aを1として扱うホイール対応)
    uniq = sorted(set(vals), reverse=True)
    def straight_high(vs):
        run = 1
        best = None
        for i in range(len(vs)-1):
            if vs[i] - 1 == vs[i+1]:
                run += 1
                if run >= 5:
                    best = vs[i-3]
            elif vs[i] != vs[i+1]:
                run = 1
        # A-5ストレート
        if set([14,5,4,3,2]).issubset(set(vals)):
            best = max(best or 0, 5)
        return best
    
    straight_hi = straight_high(uniq)

    if is_flush and straight_hi:
        return (8, straight_hi)
    if by_count[0][1] == 4:
        four = by_count[0][0]
        kicker = max([v for v in vals if v != four])
        return (7, four, kicker)
    if by_count[0][1] == 3 and any(c==2 for _,c in by_count[1:]):
        triple = by_count[0][0]
        pair = max([v for v,c in by_count[1:] if c==2])
        return (6, triple, pair)
    if is_flush:
        return (5, *vals)
    if straight_hi:
        return (4, straight_hi)
    if by_count[0][1] == 3:
        triple = by_count[0][0]
        kickers = [v for v in vals if v != triple][:2]
        return (3, triple, *kickers)
    pairs = [v for v,c in by_count if c==2]
    if len(pairs) >= 2:
        top, sec = sorted(pairs, reverse=True)[:2]
        kicker = max([v for v in vals if v not in [top,sec]])
        return (2, top, sec, kicker)
    if len(pairs) == 1:
        pair = pairs[0]
        kickers = [v for v in vals if v != pair][:3]
        return (1, pair, *kickers)
    return (0, *vals)

def best_hand_7(cards7: List[str]) -> Tuple[Tuple, List[str]]:
    """7枚から最強の5枚役を評価"""
    best = None
    best5 = None
    for comb in itertools.combinations(cards7, 5):
        h = hand_rank(comb)
        if (best is None) or (h > best):
            best = h
            best5 = list(comb)
    return best, best5

# ========= プレイヤークラス =========
class Player:
    def __init__(self, name: str, chips: int = INITIAL_CHIPS):
        self.name = name
        self.chips = chips
        self.hand = []
        self.folded = False
        self.allin = False
        self.active = True
        self.bet_this_round = 0
        self.has_acted = False
    
    def reset_for_new_hand(self, hand: List[str]):
        """新しいハンド用にリセット"""
        if self.chips <= 0:
            self.active = False
            self.folded = True
            self.allin = False
            self.hand = []
            self.bet_this_round = 0
            self.has_acted = True
        else:
            self.folded = False
            self.allin = False
            self.hand = hand
            self.bet_this_round = 0
            self.has_acted = False
    
    def reset_for_new_round(self):
        """新しいラウンド用にリセット"""
        self.bet_this_round = 0
        self.has_acted = False
    
    def can_act(self) -> bool:
        """行動可能かどうか"""
        return self.active and not self.folded and not self.allin
    
    def post_blind(self, amount: int) -> int:
        """ブラインドを支払う"""
        actual = min(self.chips, amount)
        self.chips -= actual
        self.bet_this_round += actual
        if self.chips == 0:
            self.allin = True
        return actual

# ========= ゲームエンジンクラス =========
class PokerGame:
    def __init__(self, player_names: List[str]):
        self.players = [Player(name) for name in player_names]
        self.dealer_index = 0
        self.sb_index = 0
        self.bb_index = 0
        self.deck = []
        self.community = []
        self.pot = 0
        self.phase = "preflop"  # preflop, flop, turn, river, showdown
        self.current_bet = 0
        self.current_index = 0
        self.last_raiser = None
        self.message = ""
        self.game_over = False
    
    def new_hand(self):
        """新しいハンドを開始"""
        self.deck = create_deck()
        random.shuffle(self.deck)
        self.community = []
        self.pot = 0
        self.phase = "preflop"
        self.current_bet = 0
        self.last_raiser = None
        
        # ディーラーボタン順回し
        self.dealer_index = (self.dealer_index + 1) % len(self.players)
        self.sb_index = (self.dealer_index + 1) % len(self.players)
        self.bb_index = (self.dealer_index + 2) % len(self.players)
        
        SB_amount = 10
        BB_amount = 20
        
        # 配牌と状態リセット
        for player in self.players:
            if player.chips <= 0:
                player.reset_for_new_hand([])
            else:
                hand = [self.deck.pop(), self.deck.pop()]
                player.reset_for_new_hand(hand)
        
        # ブラインドを支払う
        sb_posted = self.players[self.sb_index].post_blind(SB_amount)
        bb_posted = self.players[self.bb_index].post_blind(BB_amount)
        self.pot += sb_posted + bb_posted
        self.current_bet = BB_amount
        
        # プリフロップの最初の行動者はBBの次
        self.current_index = self._next_index(self.bb_index)
        self.message = f"New hand: Dealer={self.players[self.dealer_index].name}, SB={self.players[self.sb_index].name}({sb_posted}), BB={self.players[self.bb_index].name}({bb_posted})"
    
    def _alive_players(self) -> List[int]:
        """生存プレイヤーのインデックスリスト"""
        return [i for i, p in enumerate(self.players) if p.active and not p.folded]
    
    def _next_index(self, idx: int) -> Optional[int]:
        """次の行動可能プレイヤーのインデックス"""
        n = len(self.players)
        for step in range(1, n+1):
            j = (idx + step) % n
            if self.players[j].can_act():
                return j
        return None
    
    def _everyone_done_this_round(self) -> bool:
        """全員がこのラウンドで行動完了したか"""
        for i in self._alive_players():
            p = self.players[i]
            if p.allin:
                continue
            if p.bet_this_round != self.current_bet:
                return False
            if not p.has_acted:
                return False
        return True
    
    def _advance_phase(self):
        """次のフェーズに進む"""
        # ラウンド終了処理
        for p in self.players:
            p.reset_for_new_round()
        self.current_bet = 0
        self.last_raiser = None
        
        # コミュニティカード追加
        if self.phase == "preflop":
            for _ in range(3):  # フロップ3枚
                self.community.append(self.deck.pop())
            self.phase = "flop"
        elif self.phase == "flop":
            self.community.append(self.deck.pop())  # ターン1枚
            self.phase = "turn"
        elif self.phase == "turn":
            self.community.append(self.deck.pop())  # リバー1枚
            self.phase = "river"
        elif self.phase == "river":
            self.phase = "showdown"
        
        self.current_index = self._next_index(-1)  # 左から探索
        if self.phase == "showdown":
            self._do_showdown()
    
    def _do_showdown(self):
        """ショーダウン処理"""
        contenders = self._alive_players()
        if len(contenders) == 0:
            self.message = "All folded."
            return
        
        # 役判定
        scores = []
        for i in contenders:
            cards7 = self.players[i].hand + self.community
            score, best5 = best_hand_7(cards7)
            scores.append((score, i))
        
        # 勝者決定
        scores.sort(reverse=True, key=lambda x: x[0])
        top_score = scores[0][0]
        winners = [i for (sc, i) in scores if sc == top_score]
        
        # ポット分配
        share = self.pot // len(winners)
        for i in winners:
            self.players[i].chips += share
        
        self.message = f"Showdown! Winners: {', '.join(self.players[i].name for i in winners)} (+{share} each)"
        self.pot = 0
        
        # 脱落処理
        for p in self.players:
            if p.chips <= 0:
                p.active = False
                p.folded = True
                p.allin = False
        
        # ゲーム終了判定
        active_count = sum(1 for p in self.players if p.active)
        player_alive = any(p.active for p in self.players if p.name == "You")
        self.game_over = not (player_alive and active_count >= 2)
    
    def legal_actions(self, player_index: int) -> List[str]:
        """プレイヤーが取れる合法的なアクション"""
        player = self.players[player_index]
        to_call = max(0, self.current_bet - player.bet_this_round)
        
        if player.chips <= 0:
            return ["check"]
        
        actions = []
        if to_call == 0:
            actions += ["check", "bet", "raise", "fold", "allin"]
        else:
            actions += ["call", "raise", "fold", "allin"]
        
        return actions
    
    def apply_action(self, player_index: int, action: str) -> bool:
        """アクションを適用"""
        player = self.players[player_index]
        to_call = max(0, self.current_bet - player.bet_this_round)
        
        if action == "fold":
            player.folded = True
            player.has_acted = True
            self.message = f"{player.name} folds."
            
        elif action == "check":
            if to_call != 0:
                self.message = f"{player.name} cannot check (must call {to_call})."
                return False
            player.has_acted = True
            self.message = f"{player.name} checks."
            
        elif action == "call":
            pay = min(to_call, player.chips)
            player.chips -= pay
            player.bet_this_round += pay
            self.pot += pay
            if player.chips == 0:
                player.allin = True
            player.has_acted = True
            self.message = f"{player.name} calls {pay}."
            
        elif action == "bet":
            if self.current_bet != 0:
                return self.apply_action(player_index, "raise")
            amount = min(10, player.chips)
            player.chips -= amount
            player.bet_this_round += amount
            self.pot += amount
            self.current_bet = player.bet_this_round
            self.last_raiser = player_index
            if player.chips == 0:
                player.allin = True
            # 他プレイヤーの has_acted をリセット
            for j in self._alive_players():
                self.players[j].has_acted = (j == player_index)
            self.message = f"{player.name} bets {amount}."
            
        elif action == "raise":
            if player.chips <= to_call:
                if player.chips < to_call:
                    return self.apply_action(player_index, "allin")
                else:
                    return self.apply_action(player_index, "call")
            call_amt = to_call
            raise_extra = min(10, player.chips - call_amt)
            total = call_amt + raise_extra
            player.chips -= total
            player.bet_this_round += total
            self.pot += total
            self.current_bet = max(self.current_bet, player.bet_this_round)
            self.last_raiser = player_index
            if player.chips == 0:
                player.allin = True
            # 他プレイヤーの has_acted をリセット
            for j in self._alive_players():
                self.players[j].has_acted = (j == player_index)
            self.message = f"{player.name} raises to {player.bet_this_round}."
            
        elif action == "allin":
            need = max(0, self.current_bet - player.bet_this_round)
            all_amount = need + player.chips
            player.chips = 0
            player.bet_this_round += all_amount
            self.pot += all_amount
            player.allin = True
            self.current_bet = max(self.current_bet, player.bet_this_round)
            self.last_raiser = player_index
            # 他プレイヤーの has_acted をリセット
            for j in self._alive_players():
                self.players[j].has_acted = (j == player_index)
            self.message = f"{player.name} goes all-in ({player.bet_this_round})."
            
        else:
            self.message = "Unknown action."
            return False
        
        # 次のプレイヤーへ
        self.current_index = self._next_index(player_index)
        
        # ラウンド終了チェック
        alive_count = len(self._alive_players())
        if alive_count <= 1:
            # 残り1人なら即勝利
            self.phase = "showdown"
            winners = self._alive_players()
            if winners:
                winner = self.players[winners[0]]
                winner.chips += self.pot
                self.message = f"{winner.name} wins (others folded). +{self.pot}"
                self.pot = 0
            self._do_showdown()
            return True
        
        if self._everyone_done_this_round():
            self._advance_phase()
        
        return True
    
    def get_current_player(self) -> Optional[Player]:
        """現在の行動プレイヤー"""
        if self.current_index is None:
            return None
        return self.players[self.current_index]






bot_strategy.py
import random

RANK_ORDER = {r:i for i,r in enumerate(list("23456789TJQKA"), start=2)}

def approx_hand_strength_preflop(hand):
    # hand: ["As","Kd"] のような2枚
    r1, r2 = hand[0][0], hand[1][0]
    s1, s2 = hand[0][1], hand[1][1]
    pair = (r1 == r2)
    suited = (s1 == s2)
    v1, v2 = RANK_ORDER[r1], RANK_ORDER[r2]
    high = max(v1, v2)
    low  = min(v1, v2)

    score = 0
    if pair: score += 30 + high
    if suited: score += 5
    score += high/2 + low/5
    return score  # だいたい 10〜50 程度

def decide_strengthish_action(strength, to_call, chips, can_check):
    """ごく簡単な方針:
    - 強: レイズ/ベット or オールイン
    - 中: コール/チェック
    - 弱: フォールド(ただしチェックできるならチェック)
    """
    if chips <= 0:
        return "check" if can_check else "call"

    if strength >= 45:
        if chips <= to_call:  # ほぼショートならオールイン
            return "allin"
        return "raise" if to_call > 0 else "bet"
    elif strength >= 35:
        return "check" if can_check else ("call" if to_call <= chips else "fold")
    else:
        return "check" if can_check else ("fold" if to_call > 0 else "check")

class BotStrategy:
    def __init__(self, name):
        self.name = name

    def decide_action(self, hand, community_cards, chips, current_bet, bet_this_round):
        # to_call: ラウンド内で必要な追加額
        to_call = max(0, current_bet - bet_this_round)
        can_check = (to_call == 0)

        # 超簡易:プリフロップは上の関数、以降はランダム寄り + コール重視
        if len(community_cards) == 0:
            strength = approx_hand_strength_preflop(hand)
        else:
            # 盤面が出たらレンジ広めに
            strength = random.uniform(25, 50)

        action = decide_strengthish_action(strength, to_call, chips, can_check)

        # たまにブラフ/ミックス
        if action in ("check","call") and not can_check and random.random() < 0.07 and chips > to_call + 10:
            action = "raise"
        if action == "raise" and chips < to_call + 10:
            action = "call" if chips >= to_call else "allin"
        if action == "bet" and chips < 10:
            action = "allin"

        return action




3つのファイルをコピペして作って
下記のコマンドを実行すると動くと思います。

streamlit run main.py
スクリーンショット 2025-08-23 17.07.28


自分のターンの際に
Callなどのラジオボタンで行動選択
Submit Actionボタンで行動決定です。

1ゲーム終わったら
左上の「New Hand」で次のゲームへ
チップがなくなったら「Restart Game」でゲームリセット

一応、バグがちょろちょろ有るので
ちゃんとは動かないところもあるかもしれませんが
ちょっと試してみるは出来ると思います。


自分で自分のBOTと対戦するも良し
BOTを複数繋げて対戦させ、一番強いBOTを探るもよし
色々改造して遊べると思います。

今日はここまでです
それでは

今回は最近流行りのポーカー
に関する確率のシミュレーションです

解説動画はこちら


 

ポーカーについて

ポーカーはトランプ5枚の手札の組み合わせで
役を作るゲームです。

1.ハイカード(強いカードの所持 2 < A)
2.ワンペア(同じ数字の組み合わせが1つ)
3.ツーペア(同じ数字の組み合わせが2つ)
4.スリーカード(同じ数字3つ)
5.ストレート(2,3,4,5,6 などの数字の並び)
6.フラッシュ(同じスートの組み合わせ HDCS)
7.フルハウス(スリーカードに加えて、同じ数字の組み合わせが1つ)
8.フォーカード(同じ数字4つ)
9.ストレートフラッシュ(ストレート + フラッシュ)
10.ロイヤルフラッシュ(AKQJTのフラッシュ)


テキサスホールデムルール


ポーカーのルールの一つで
プレイヤーそれぞれに配られた2枚のカードと
プレイヤー全員が共有する公開された
コミュニティカード"枚の計7枚で役を作り
チップをベットするなどの駆け引きを行うゲームルールです
(ベット周りの詳細なルールは割愛)


ここからはテキサスホールデムの
初期手札による勝率がどうなるのかを
検証するシミュレーションプログラムについてです。




役を計算するプログラム

シミュレーションを行うには
ポーカーの役の判定を行うプログラムが必要ですね

import random
from collections import Counter
from itertools import combinations

# ポーカーの役
POKER_HANDS = {
    0: "ハイカード",
    1: "ワンペア",
    2: "ツーペア",
    3: "スリーカード",
    4: "ストレート",
    5: "フラッシュ",
    6: "フルハウス",
    7: "フォーカード",
    8: "ストレートフラッシュ",
    9: "ロイヤルストレートフラッシュ"
}

# カードのランクとスート(HA=ハートのエース, D2=ダイヤの2)
RANKS = "23456789TJQKA"
SUITS = "HDSC"  # ハート, ダイヤ, スペード, クラブ
DECK = [s + r for r in RANKS for s in SUITS]

# 役の評価(スコアをタプルで返す)
def evaluate_hand(hand):
    ranks = sorted([RANKS.index(c[1]) for c in hand], reverse=True)
    suits = [c[0] for c in hand]
    rank_counts = Counter(ranks)
    flush = len(set(suits)) == 1
    straight = len(rank_counts) == 5 and (max(ranks) - min(ranks) == 4 or set(ranks) == {12, 3, 2, 1, 0})  # A-2-3-4-5対応

    # 役の判定(同率はRankで比較)
    if straight and flush:
        if set(ranks) == {12, 11, 10, 9, 8}:  # A, K, Q, J, 10
            return (9, max(ranks))  # ロイヤルストレートフラッシュ
        return (8, max(ranks)) # ストレートフラッシュ
    if 4 in rank_counts.values():
        return (7, max(k for k, v in rank_counts.items() if v == 4))  # フォーカード
    if 3 in rank_counts.values() and 2 in rank_counts.values():
        return (6, max(k for k, v in rank_counts.items() if v == 3))  # フルハウス
    if flush:
        return (5, ranks)  # フラッシュ
    if straight:
        return (4, max(ranks))  # ストレート
    if 3 in rank_counts.values():
        return (3, max(k for k, v in rank_counts.items() if v == 3))  # スリーカード
    if list(rank_counts.values()).count(2) == 2:
        return (2, sorted([k for k, v in rank_counts.items() if v == 2], reverse=True))  # ツーペア
    if 2 in rank_counts.values():
        return (1, max(k for k, v in rank_counts.items() if v == 2))  # ワンペア
    return (0, ranks)  # ハイカード

これを用いてシミュレーションを行なっていきます。



テキサスホールデムのシミュレーション

初期手札2枚と公開札5枚で、役を作り、勝率がどうなるか
4人で対戦をする際の勝率を出す
シミュレーションプログラムです。

user_num のところが対戦人数になるので
変更すれば、その人数での確率を求めることができます。
# モンテカルロ法で勝率計算
def monte_carlo_win_rate(my_hand, num_simulations=10000):
    wins = 0
    user_num = 3
    for _ in range(num_simulations):
        deck = DECK.copy()
        for card in my_hand:
            deck.remove(card)

        # 4人分の手札をランダムに配る
        random.shuffle(deck)
        opponent_hands = [deck[i * 2: (i + 1) * 2] for i in range(user_num)]
        community_cards = deck[6:11]

        # 各プレイヤーのベストハンドを評価
        my_best = max(evaluate_hand(list(comb)) for comb in combinations(my_hand + community_cards, 5))
        opponent_best = [max(evaluate_hand(list(comb)) for comb in combinations(hand + community_cards, 5)) for hand in opponent_hands]

        # 自分が最も強い手を持っているか判定
        if my_best > max(opponent_best):
            wins += 1

    return wins / num_simulations

# 例: 自分の手札をセットして勝率を計算
my_hand = ["HA", "HK"]  # ハートのエース・キング
win_rate = monte_carlo_win_rate(my_hand, num_simulations=5000)
print(f"勝率: {win_rate:.2%}")
勝率: 33.02%

カードの組み合わせを変えれば
その都度計算が行えます。



初期カードの組み合わせでの勝率

2種の13 * 13 枚の組み合わせにおける
勝率を計算してみましょう。

RANKS = "23456789TJQKA"
H_DECK = [s + r for r in RANKS for s in "H"]
D_DECK = [s + r for r in RANKS for s in "D"]
combi = [[h, d] for h in H_DECK for d in D_DECK]
result = {":".join(c):0 for c in combi}
for my_hand in combi:
  result[":".join(my_hand)] = monte_carlo_win_rate(my_hand, num_simulations=5000)

これで、13x13=169通りの結果が出せます。

これをヒートマップにしてみましょう。


勝率をヒートマップにする


import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# キーのソート用の関数
def get_rank_order(card):
    return RANKS.index(card[1])

# キーを分離してデータフレームを作成
data_list = []
for key, value in result.items():
    row_key, col_key = key.split(':')
    data_list.append({'row': row_key, 'col': col_key, 'value': value})

df = pd.DataFrame(data_list)
unique_rows = sorted(df['row'].unique(), key=get_rank_order)
unique_cols = sorted(df['col'].unique(), key=get_rank_order)

# ピボットテーブルを作成
pivot_df = df.pivot(index='row', columns='col', values='value')

# インデックスと列を順序通りに並び替え
pivot_df = pivot_df.reindex(index=unique_rows, columns=unique_cols)

# ヒートマップの作成
plt.figure(figsize=(10, 8))
sns.heatmap(pivot_df,
            cmap='RdYlGn',
            vmin=0,
            vmax=1,
            annot=True,
            fmt='.2f',
            cbar_kws={'label': 'Value'})

plt.title('Win rate for first hand combination')
plt.tight_layout()
plt.show()
heatmap

やはり、最初にワンペアを持っているだけで勝率は高くなりますね
とはいえ、それだけでは勝てないのが
このテキサスホールデムルールの面白いところ

初期手札の読み合いやベットなどの駆け引き
この辺りが組み合わさることで
ゲーム性が高くかなり面白いものになっています。

とはいえ
日本ではまだまだ、カジノがないので
ポーカーを楽しむには
単純なゲームとしてのポーカーしか出来ません

オンラインで展開されているものは
ほぼ日本国内では違法ではあるので
手を出さないように気を付けないといけません!!!

カジノが出来たら
もっともっとシミュレーションしましょう。

今回はここまでです
それでは

このページのトップヘ