今回はオンカジに有りそうな
ポーカー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を探るもよし
色々改造して遊べると思います。

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