今回はオンカジに有りそうな
ポーカーBOTについてです。
解説動画はこちら
注
この内容はあくまでも
BOTプログラムに関する内容となります。
プログラムの内容
ポーカーのテキサスホールデムルールのゲームを行える
Streamlitプログラムです。
BOT用のストラテジークラスがあり
それを改変することでBOTの戦略を
構築することができるようになっています。
テキサスホールデムのルール
ゲームはブラインドベットで開始され
プリフロップ、フロップ、ターン、リバーの
4つのラウンドでそれぞれベットが行われる
今回のBOTの特徴
プリフロップはそれなり
ポストフロップは運任せ
という戦略です。
細かいルールは
ハンド強度評価を行い
3段階の強度分類でアクションを決めます。
強度分類
こんな戦略をとるBOTです。
プログラムの詳細
3つのプログラムからなっています。
main.py
poker_engine.py
bot_strategy.py
3つのファイルをコピペして作って
下記のコマンドを実行すると動くと思います。

自分のターンの際に
Callなどのラジオボタンで行動選択
Submit Actionボタンで行動決定です。
1ゲーム終わったら
左上の「New Hand」で次のゲームへ
チップがなくなったら「Restart Game」でゲームリセット
一応、バグがちょろちょろ有るので
ちゃんとは動かないところもあるかもしれませんが
ちょっと試してみるは出来ると思います。
自分で自分のBOTと対戦するも良し
BOTを複数繋げて対戦させ、一番強いBOTを探るもよし
色々改造して遊べると思います。
今日はここまでです
それでは
ポーカーBOTについてです。
解説動画はこちら
注
この内容はあくまでも
BOTプログラムに関する内容となります。
プログラムの内容
ポーカーのテキサスホールデムルールのゲームを行える
Streamlitプログラムです。
BOT用のストラテジークラスがあり
それを改変することでBOTの戦略を
構築することができるようになっています。
テキサスホールデムのルール
自分だけの手札2枚と場に共有される5枚のカード
(コミュニティカード)を組み合わせて
最も強い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

自分のターンの際に
Callなどのラジオボタンで行動選択
Submit Actionボタンで行動決定です。
1ゲーム終わったら
左上の「New Hand」で次のゲームへ
チップがなくなったら「Restart Game」でゲームリセット
一応、バグがちょろちょろ有るので
ちゃんとは動かないところもあるかもしれませんが
ちょっと試してみるは出来ると思います。
自分で自分のBOTと対戦するも良し
BOTを複数繋げて対戦させ、一番強いBOTを探るもよし
色々改造して遊べると思います。
今日はここまでです
それでは

