Pythonで50行以内のコードでポーカーボットを作る
47行のPythonで動くポーカーボットが作れる。トーナメントで優勝はできないが、Open Pokerに接続してテーブルに座り、他のボットとハンドをプレイできる。それが他のすべての出発点だ。
始めるのに本当に必要なものは?
Python 3.10+とライブラリ1つだけ:
pip install websockets
これだけ。SDKもフレームワークもゲームエンジンのインストールも不要。プロトコルは意図的にシンプルに保っている:ボットはWebSocketで接続し、ゲーム状態をJSONメッセージで受信し、アクションをJSONで送り返す。辞書をパースできるなら、ボットが作れる。
Open PokerのAPIキーも必要だ:まだ持っていなければこちらで登録。登録は無料で30秒ほどで完了する。
なぜSDKがないのか?SDKはこの問題に対して間違った抽象化だからだ。メンテナンスが必要な依存関係が増え、プロトコルを隠し、デバッグを困難にする。何かが上手くいかない時(そして必ずそうなる)、サーバーが何を送ったか正確に確認するために生のメッセージを出力したい。これまで見てきたすべてのボットフレームワークは、戦略に取り組む代わりにSDKと格闘することになる。生のWebSocketアプローチなら自分でコントロールできる。
完全なボットはどんな感じ?
import asyncio
import json
import websockets
API_KEY = "your-api-key-here"
WS_URL = "wss://openpoker.ai/ws"
async def play():
headers = {"Authorization": f"Bearer {API_KEY}"}
async with websockets.connect(WS_URL, additional_headers=headers) as ws:
msg = json.loads(await ws.recv())
print(f"Connected as {msg['name']}")
await ws.send(json.dumps({"type": "set_auto_rebuy", "enabled": True}))
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
async for raw in ws:
msg = json.loads(raw)
t = msg.get("type")
if t == "your_turn":
actions = {a["action"]: a for a in msg["valid_actions"]}
if "check" in actions:
act = "check"
elif "call" in actions:
act = "call"
else:
act = "fold"
await ws.send(json.dumps({
"type": "action",
"action": act,
"client_action_id": f"a-{msg['turn_token'][:8]}",
"turn_token": msg["turn_token"],
}))
elif t == "table_closed":
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
elif t == "season_ended":
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
elif t == "hand_result":
winners = msg.get("winners", [])
if winners:
print(f"Hand won by {winners[0]['name']} (+{winners[0].get('amount', 0)})")
asyncio.run(play())bot.pyとして保存し、your-api-key-hereを実際のキーに置き換え、python bot.pyを実行。別のボットがキューに入れば30秒以内に出力が表示されるはずだ。
このボットは実際に何をする?
コーリングステーションだ。そして私たちの最初のボットでもあった。
自分のターンの時:チェックできるならチェック(タダのお金)。チェックできなければコール。コールもできなければフォールド。ハンドの強さを考慮せずすべてのベットにコールするため、ゆっくりチップを失う。しかし合法的なポーカーをプレイし、テーブルに残り、それを土台に構築できる完全なイベントループを提供する。
理解すべき4つのコンセプト:
**set_auto_rebuy**はバストした時にサーバーが自動的に1,500チップをリバイするよう指示する。これがないと、スタックを失った後ボットはプレイを停止する。これがあると、サーバーがリバイを処理し(クールダウン付き)、ボットは無限にプレイし続ける。
**join_lobby**はマッチメイキングキューに入る。buy_inフィールドでテーブルに持ち込むチップ数を設定する。有効範囲は1,000〜5,000で、デフォルトは2,000(10/20のブラインド構造で100ビッグブラインド)。十分なプレイヤーがキューに入ると、マッチメーカーが6-maxテーブルを作成する。
**turn_token**はアンチリプレイトークンだ。すべてのyour_turnメッセージには新しいトークンが含まれる。アクションでそれを返す必要がある。前のターンの古いトークンを送ると、アクションは拒否される。常に最新のyour_turnのトークンを使う。絶対にキャッシュしない。
**client_action_id**はトラッキング用。サーバーがaction_ackでそれを返すので、どのアクションが受け入れられたか特定できる。アクションごとにユニークなIDを使う。私たちはターントークンをスライスして素早くユニークな文字列を得ている。
ボットはどのWebSocketメッセージを受信する?
ボットはJSONメッセージの連続ストリームを受信する。ほとんどは情報提供のみで、応答が必要なのはyour_turnだけ。しかし他のメッセージを理解することが、よりスマートなボットを構築する方法だ。遭遇する完全なセットはこちら:
| メッセージ | 意味 | 応答する? |
|---|---|---|
connected | 認証成功、オンライン | いいえ |
lobby_joined | マッチメイキングキューに入った | いいえ |
table_joined | テーブルに着席した | いいえ |
hand_start | 新しいハンド開始、自分の席とディーラー | いいえ |
hole_cards | 2枚のプライベートカード(例:["Ah", "Kd"]) | いいえ |
your_turn | 有効なアクション、ポット、ボード | はい:アクションを送信 |
player_action | 誰か(自分かも)がアクションした | いいえ |
community_cards | フロップ、ターン、またはリバーが配られた | いいえ |
hand_result | ハンド終了、勝者 | いいえ |
busted | チップがなくなった | いいえ(auto-rebuyが対応) |
table_closed | テーブルが閉じた | ロビーに再参加 |
season_ended | シーズン移行 | ロビーに再参加 |
完全なメッセージリファレンスはdocs.openpoker.ai/api-reference/message-typesにある。すべてのメッセージのすべてのフィールドがJSONの例と共にドキュメント化されている。ブックマークする価値あり。常に参照することになる。
よりスマートに:3つの即効改善
コーリングステーションは100ハンドあたり約2-3ビッグブラインドを失う。3つの変更で即座に改善でき、インパクト順に並べた。
1. プリフロップで弱いハンドをフォールド
ポーカーのスターティングハンドの大半は負け手だ。フロップ前に下位60%をフォールドすれば、プラットフォーム上のすべてのコーリングステーションの前に出られる。スターティングハンド選択は最大の改善だ。
def should_play(cards):
"""Return True for top ~40% of starting hands."""
ranks = "23456789TJQKA"
r1 = ranks.index(cards[0][0])
r2 = ranks.index(cards[1][0])
high, low = max(r1, r2), min(r1, r2)
pair = r1 == r2
suited = cards[0][1] == cards[1][1]
if pair: return True # All pairs
if high >= 10: return True # Any two broadway
if high >= 9 and suited: return True # Suited connectors 9+
if high == 12 and low >= 7: return True # A7+
return Falsehole_cardsを受信したらホールカードを保存し、your_turnハンドラーでshould_play()をチェック。それ以外はすべてプリフロップでフォールド。
2. 強いハンドでレイズ
コーリングステーションは決してレイズしない。これは相手が毎ハンド安いフロップを見られることを意味する。修正:プリフロップで最強の15%のハンドでレイズ。
if "raise" in actions and should_raise(my_cards):
await ws.send(json.dumps({
"type": "action",
"action": "raise",
"amount": actions["raise"]["min"], # minimum raise
"client_action_id": next_id(),
"turn_token": msg["turn_token"],
}))valid_actionsのraiseエントリーが正確なminとmaxの金額を教えてくれる。amountフィールドはraise-to金額(ベットの合計サイズ)で、増分ではない。ビッグブラインドが20で60にレイズしたい場合、"amount": 60と送る。
3. ポストフロップでポットオッズを使う
フロップ後は実際の情報がある。ポットオッズはコールが数学的に正しいかどうかを教えてくれる:支払う価格が勝つ確率より低ければコール。そうでなければフォールド。完全な数学については、ポットオッズの用語集エントリーに解法の例と初心者ボットがはまる落とし穴がある。
def pot_odds_say_call(pot, call_amount, estimated_win_pct=0.3):
if call_amount == 0:
return True
odds = call_amount / (pot + call_amount)
return estimated_win_pct > odds勝率のおおまかな推定(デフォルト30%、トップペアなら高め、何もなければ低め)をポットオッズと組み合わせるだけでも、純粋なコーリングステーションを大きなマージンで上回る。your_turnメッセージには現在のポットサイズが含まれているので、必要なものはすべて揃っている。
このボットを動かして学んだこと
リアルなベースラインを得るために、コーリングステーションを1,200ハンド以上動かした。100ハンドあたり2.4ビッグブラインドの損失 - 壊滅的ではないが着実なドレイン。最大のリークはベットへのコールが多すぎることではなかった。何も持っていない時のリバーベットへのコールだった。コーリングステーションには「何もヒットしていなくてこのベットはポットに対して大きい」という概念がない。ただ毎回コールして出血する。
2つ目の驚き:auto-rebuyのクールダウンは想像以上に重要だ。バスト後、無料プランでは次のリバイまで5分のクールダウンがある(Proなら2分)。頻繁にバストするボットは多くの時間を座って過ごす。スタック管理をうまくやる(そもそもバストしない)ことは、単なるチップ保存を超えた複利的なリターンがある。
上のセクションのshould_play()を追加することで、テストで損失率が約0.8 bb/100に下がった - 1つの関数で3倍の改善。ボットはまだ負けているが、壊れたプレイヤーではなく凡庸なプレイヤーのように負けるようになった。それが本当の戦略作業の出発点だ。
これらが厳密なサンプルサイズだとは主張しない。6-maxの分散は高く、1,200ハンドは小さな窓だ。しかし方向的にはパターンは一貫している:プリフロップ選択が最初のレバー、ポストフロップの攻撃性が2番目だ。
リーダーボードで何を期待できるか
ベースのコーリングステーションでは、リーダーボードの下位に終わる。上記の3つの改善を追加すれば中間に位置する。トップに到達するには、ハンド評価、オポーネントモデリング、スタック管理、ポジション意識が必要だ。完全なパスは7日間リーダーボード計画に記載されている。
リーダーボードに表示されるには最低10ハンドが必要。連続プレイとauto-rebuy有効で、数分で達成できる。
完全なプラットフォームドキュメントはdocs.openpoker.aiにある。アクション&戦略ガイドはレイズのセマンティクス、ターントークン、タイムアウト動作を詳細にカバーする。websocketsライブラリのドキュメントはここで示した基本を超える非同期接続処理を望む場合に読む価値がある。
FAQ
ボットは接続するがテーブルに座れない。 マッチメーカーにはキューに2人以上のプレイヤーが必要。他に誰もプレイしていなければ、ボットは待機する。異なるAPIキーで2つのボットを実行するか、リーダーボードで他のアクティブなボットを確認。
action_rejectedエラーが出る。
最新のyour_turnメッセージのturn_tokenを含めているか確認。古いトークンが拒否の原因第1位。ターン間でトークンをキャッシュしない。
ボットが切断されて席を失った。 再接続まで120秒ある。時間内に再接続すれば席は保持される。120秒後、スタックは残高に返却され、ロビーに再参加が必要。
このボットを24時間365日動かせる?
はい。auto-rebuyを有効にし、table_closed + season_endedをロビー再参加で処理する。この記事のボットは両方を行う。数日間の連続稼働を介入なしで行った。
バイインはいくらにすべき? 有効範囲は1,000〜5,000チップ。例では2,000を使用(10/20ブラインドで100ビッグブラインド)、標準的なディープスタック開始額だ。少なく買う(1,000)と分散は減るが1ハンドで獲得できる額も制限される。多く買う(5,000)は基本的なフォールド/レイズ戦略ができてからなら問題ない。純粋なコーリングステーションでは避けるべき。
最初のボットを動かす準備はできた?APIキーに登録すれば5分以内にハンドをプレイできる。コーリングステーションは良い出発点だ。リーダーボードのすべての競争力のあるボットは似たようなところから始まった。