Skip to content
[OPEN_POKER]

ポーカーボットのデバッグ:よくある7つのWebSocketエラーの修正方法

JJoão Carvalho||16 min read

すべてのボットビルダーが同じポーカーボットの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_tokenyour_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配列がレイズの正確なminmaxを示します。その範囲外のものを送信すると拒否されます。レイズサイズをハードコードしないでください。

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_actionsfoldcallのみを含む場合、checkの送信は拒否されます。常に配列を読んでください。利用可能なものを決して仮定しないでください。

4. クラッシュせずにnullフィールドを処理するには?

Open Pokerのプロトコルでは、いくつかのフィールドは省略されるのではなく、値nullで存在します。これは意図的な設計選択(一貫したメッセージ形式)ですが、素朴な型変換を使用するボットビルダーを罠にかけます。

典型的なクラッシュ:

# amountがnullの場合TypeErrorが発生
amount = float(msg["amount"])  # float(None) -> TypeError

player_actionメッセージはフォールドとチェックでamountnullに設定します。修正は簡単です:

# 安全:nullを処理
amount = msg.get("amount") or 0.0

to_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_lobbyjoin_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_idlast_table_seqを含むresync_requestを送信して、見逃したイベントを回復してください。まだあなたのターンであれば、現在のゲーム状態を含む新しいyour_turnメッセージを受け取ります。

なぜinvalid_messageエラーが出るのですか? JSONが不正であるか、必須フィールドが欠けています。一般的な原因:ダブルクォートの代わりにシングルクォート(Pythonのjson.dumpsはこれを処理しますが、f-stringは処理しません)、typeフィールドの欠落、またはJSONにシリアライズせずにPython dictを直接送信すること。


ここでカバーされていないバグがありますか?完全なプロトコルリファレンスを読むか、ボットを登録してライブサーバーに対してデバッグを始めてください。プロトコルを学ぶ最良の方法は、すべてのメッセージを出力して読むことです。

続きを読む