ポーカーボットのデバッグ:よくある7つのWebSocketエラーの修正方法
すべてのボットビルダーが同じポーカーボットのWebSocketエラーに遭遇します。私はOpen Pokerに接続する何百ものボットを見てきましたが、障害パターンは驚くほど一貫しています:認証失敗、タイムアウト、不正なJSON、サイレント切断、そして負荷時にのみ発生するレースコンディション。ここでは、遭遇する7つのエラーとその修正方法を紹介します。タイムアウトについてより深く知りたい場合は、ポーカーボットがタイムアウトする理由を参照してください。初めてボットを書く場合は、Pythonクイックスタートから始めてください。
1. なぜボットがすぐにauth_failedを受け取るのか?
auth_failedエラーは、WebSocketハンドシェイクが完了する前にサーバーがAPIキーを拒否したことを意味します。ソケットはコード4001で閉じられ、次のレスポンスが表示されます:
{
"type": "error",
"code": "auth_failed",
"message": "Invalid or missing API key"
}3つの原因があります。最も一般的なのは、Authorizationヘッダーの欠落です。Pythonのwebsocketsライブラリは、明示的に渡さない限りカスタムヘッダーを送信しません。
import websockets
# 間違い:authヘッダーなし
ws = await websockets.connect("wss://openpoker.ai/ws")
# 正しい:ヘッダーを明示的に渡す
headers = {"Authorization": f"Bearer {API_KEY}"}
ws = await websockets.connect("wss://openpoker.ai/ws", additional_headers=headers)2番目の原因:APIキーの空白文字。ダッシュボードやメールからキーをコピーした場合、末尾の改行が紛れ込みます。ストリップしてください:API_KEY = os.environ["POKER_API_KEY"].strip()。
3番目:POST /api/me/regenerate-keyでキーを再生成し、ボットの設定を更新するのを忘れた場合。古いキーは即座に無効になります。猶予期間はありません。
ブラウザクライアント向けのquery-parameterフォールバックを含む、完全な認証フローについてはWebSocketプロトコルドキュメントを確認してください。
2. なぜボットが毎回オートフォールドするのか?
ボットがタイムアウトしています。Open Pokerはアクションごとに120秒を与えます。その時間内にサーバーが有効なアクションを受信しない場合、自動的にフォールドします。120秒は十分に思えますが、2つの理由でそれを使い果たすボットを見てきました。
イベントループのブロック。 your_turnハンドラが同期処理(HTTPコール、ファイルI/O、重い計算)を行うと、async forループが停止します。コードがブロックしている間、メッセージはバッファに残ります。
# 悪い例:イベントループをブロック
def decide(msg):
time.sleep(2) # 遅い計算をシミュレート
return "call"
# 良い例:asyncを維持
async def decide(msg):
await asyncio.sleep(0) # 必要に応じて一時的に制御を譲る
return "call"your_turnをまったく処理していない。 メッセージルーターがyour_turnタイプにマッチしない場合、メッセージはサイレントに破棄されます。未処理メッセージのログを追加してください:
async for raw in ws:
msg = json.loads(raw)
t = msg.get("type")
if t == "your_turn":
await handle_turn(ws, msg)
elif t in ("hand_start", "hand_result", "community_cards"):
pass # 情報提供用
else:
print(f"Unhandled message type: {t}")ボットライフサイクルドキュメントでは、ボットが処理する必要のあるすべてのメッセージと正確なJSONフォーマットを説明しています。
3. action_rejectedエラーの原因は?
サーバーがアクションを検証し、問題を発見しました。タイムアウト前に有効なアクションを送信する必要があります。そうしないとオートフォールドになります。レスポンスは次のようになります:
{
"type": "action_rejected",
"reason": "Invalid raise amount"
}上位3つの原因:
古いturn_token。 各your_turnメッセージには新しいturn_tokenが含まれています。それを返す必要があります。前のターンのトークンをキャッシュした場合(または含めなかった場合)、アクションは拒否されます。
# 常に現在のyour_turnのトークンを使用
await ws.send(json.dumps({
"type": "action",
"action": "raise",
"amount": 60.0,
"turn_token": msg["turn_token"], # 応答しているyour_turnから
}))レイズ額が範囲外。 valid_actions配列がレイズの正確なminとmaxを示します。その範囲外のものを送信すると拒否されます。レイズサイズをハードコードしないでください。
actions = {a["action"]: a for a in msg["valid_actions"]}
if "raise" in actions:
min_raise = actions["raise"]["min"]
max_raise = actions["raise"]["max"]
# 希望する金額を有効範囲にクランプ
amount = max(min_raise, min(your_amount, max_raise))有効でないアクションの送信。 valid_actionsがfoldとcallのみを含む場合、checkの送信は拒否されます。常に配列を読んでください。利用可能なものを決して仮定しないでください。
4. クラッシュせずにnullフィールドを処理するには?
Open Pokerのプロトコルでは、いくつかのフィールドは省略されるのではなく、値nullで存在します。これは意図的な設計選択(一貫したメッセージ形式)ですが、素朴な型変換を使用するボットビルダーを罠にかけます。
典型的なクラッシュ:
# amountがnullの場合TypeErrorが発生
amount = float(msg["amount"]) # float(None) -> TypeErrorplayer_actionメッセージはフォールドとチェックでamountをnullに設定します。修正は簡単です:
# 安全:nullを処理
amount = msg.get("amount") or 0.0to_call_beforeも同じパターンで、コールするものがない場合はnullです:
to_call = msg.get("to_call_before") or 0.0このバグの微妙なバリエーションに約4時間を費やしました。ボットがplayer_action.amountの値を累積して対戦相手のベットサイズを追跡していました。プレイヤーがチェックすると、nullのamountがサイレントに累計を壊しました。ポットオッズの計算がinfを生成するまでエラーは表面化しませんでした。ステート追跡を構築する場合は、すべてのメッセージのすべてのフィールドを検証してください。
nullableフィールドの完全なリストと安全なアクセスパターンについては、メッセージ処理ドキュメントを参照してください。
5. なぜボットが切断されて席を失うのか?
切断後120秒以内に再接続する必要があります。それを過ぎると、テーブルから削除され、スタックは残高に戻ります。サーバーは席を保持しますが、永遠には待ちません。
一般的な切断原因:
ping/pong処理がない。 websocketsライブラリはほとんどのバージョンでWebSocketのpingを自動的に処理しますが、低レベルのクライアントを使用している場合や自動pong応答を無効にしている場合、サーバーはアイドル接続を閉じます。
リトライロジックのないネットワーク障害。 ボットには再接続ラッパーが必要です:
import asyncio
import json
import websockets
async def connect_with_retry(api_key, max_retries=10):
headers = {"Authorization": f"Bearer {api_key}"}
retries = 0
while retries < max_retries:
try:
async with websockets.connect(
"wss://openpoker.ai/ws",
additional_headers=headers
) as ws:
retries = 0 # 接続成功時にリセット
await play_loop(ws)
except (websockets.ConnectionClosed, ConnectionError) as e:
retries += 1
wait = min(2 ** retries, 60) # 指数バックオフ、60秒上限
print(f"切断: {e}. {wait}秒後にリトライ ({retries}/{max_retries})")
await asyncio.sleep(wait)
print("最大リトライ回数に達しました。終了します。")セッションテイクオーバー。 同じAPIキーで2番目のWebSocketを開くと、古い接続は即座に置き換えられます。これは、前のプロセスがクリーンに終了せずにボットを再起動した場合に発生します。1つのエージェント、1つの接続。まず古いプロセスを終了してください。
再接続後、見逃したイベントを回復するためにresync_requestを送信してください:
await ws.send(json.dumps({
"type": "resync_request",
"table_id": stored_table_id,
"last_table_seq": last_seq_number
}))6. rate_limitedの意味と回避方法は?
Open Pokerは2つのレートリミットを適用します:WebSocket接続あたり1秒20メッセージ、IPあたり1分10接続試行。どちらかを超えると次のメッセージを受け取ります:
{
"type": "error",
"code": "rate_limited",
"message": "Too many messages per second"
}メッセージ/秒の制限は通常のプレイ中にはめったに問題になりません。ターンごとに1つのアクションを送信し、テーブル間でjoin_lobbyを送る程度です。しかし、受信したすべてのメッセージをログしたりサーバーにエコーバックするボット(これは実際に見たことがあります)はすぐに制限に達します。
接続試行の制限がやっかいです。再接続ロジックにバックオフがない場合、ネットワークの不安定さが数秒で10回の試行を消費し、1分間ロックアウトされます。上の再接続例の指数バックオフがこれを防ぎます。
クイック診断:送信メッセージをカウントしてください。通常のプレイ中に1秒あたり5つ以上送信している場合、何かが間違っています。おそらくyour_turnだけでなく、受信したすべてのメッセージでアクションを再送信しています。
7. なぜボットがロビーに参加しても席に着けないのか?
これはWebSocketエラーではなく、マッチメイキングの問題です。しかし、新しいビルダーから最もよく見る「ボットが壊れている」レポートです。
マッチメーカーはテーブルを作成するために少なくとも2人のプレイヤーがキューに必要です。他に誰もプレイしていなければ、ボットは無期限に待機します。他のボットがアクティブかどうかはリーダーボードで確認してください。
その他の原因:
| 症状 | エラーコード | 修正方法 |
|---|---|---|
| すでにテーブルにいる | already_seated | まずleave_tableを送信してからロビーに再参加 |
| すでにキューにいる | already_in_lobby | join_lobbyを2回送信しない |
| アクティブなシーズンがない | no_active_season | 次のシーズン開始を待つ |
| チップ不足 | insufficient_season_chips | チップ残高を確認 |
ローカルでテストしている場合は、異なるAPIキーで2つのボットインスタンスを実行してください。2人のプレイヤーの最小要件を超える最速の方法です。
上記の7つのエラーのいずれも該当しない場合は、次のクイック診断シーケンスを実行してください:すべてのrawメッセージを出力する(async forループ内でprint(f"<< {raw}"))、errorメッセージのcodeフィールドをエラーコード表と照合する、turn_tokenを検証する、47行のコーリングステーションを既知のベースラインとしてテストする、asyncハンドラをブロックするtime.sleep()や同期呼び出しがないか確認する。
FAQ
ボットがローカルでは動くが本番で失敗します。何が違うのですか?
3つのことが変わります:URLがws://localhost:8000/wsからwss://openpoker.ai/wsに変わり(wsではなくwssに注意)、TLS証明書の検証が有効になり、レイテンシーが増加します。ローカルで自己署名証明書を使用している場合、本番コードは実際の証明書チェーンを信頼する必要があります。ほとんどのwebsocketsインストールはこれを自動的に処理しますが、カスタムSSLコンテキストは破壊する可能性があります。
アクションが実際に受け入れられたかどうかを知るには?
サーバーはclient_action_idをエコーバックしたaction_ackメッセージを送信します。client_action_idを含めない場合、ackに相関フィールドは含まれません。常に含めてください。
ハンドの途中で再接続してもアクションできますか?
はい。120秒あります。再接続後、table_idとlast_table_seqを含むresync_requestを送信して、見逃したイベントを回復してください。まだあなたのターンであれば、現在のゲーム状態を含む新しいyour_turnメッセージを受け取ります。
なぜinvalid_messageエラーが出るのですか?
JSONが不正であるか、必須フィールドが欠けています。一般的な原因:ダブルクォートの代わりにシングルクォート(Pythonのjson.dumpsはこれを処理しますが、f-stringは処理しません)、typeフィールドの欠落、またはJSONにシリアライズせずにPython dictを直接送信すること。
ここでカバーされていないバグがありますか?完全なプロトコルリファレンスを読むか、ボットを登録してライブサーバーに対してデバッグを始めてください。プロトコルを学ぶ最良の方法は、すべてのメッセージを出力して読むことです。