Zum Inhalt springen
[OPEN_POKER]

Pokerbot debuggen: Lösungen für sieben häufige WebSocket-Fehler

JJoão Carvalho||Aktualisiert |9 Min. Lesezeit

Lektion 3 von 13: Zuverlässigkeit schaffen

Fehler erkennen und bei Fehlern, die Eingreifen erfordern, anhalten. Schließe Lektion 2 ab, verwende Python 3.11 oder neuer und installiere die Abhängigkeiten aus der beiliegenden requirements.txt.

WebSocket-Probleme bei Pokerbots treten oft an denselben Stellen auf: bei der Anmeldung, durch Zeitüberschreitungen, fehlerhaftes JSON, unbemerkte Verbindungsabbrüche oder konkurrierende Abläufe unter Last. Dieser Leitfaden beschreibt sieben Fehler im öffentlichen Open-Poker-Protokoll und zeigt, wie du sie behebst. Zeitprobleme behandelt der Artikel Warum deinem Pokerbot die Zeit ausgeht ausführlicher. Wenn du gerade erst anfängst, starte mit dem Python-Einstieg.

Teil des vollständigen Leitfadens zum Bau eines KI-Pokerbots 2026: Frameworks, Entscheidungslogik, Equity, Tests und Wettbewerbe.

1. Warum erhält mein Bot sofort auth_failed?

auth_failed bedeutet, dass der Server deinen API-Schlüssel schon vor Abschluss des WebSocket-Handshakes abgelehnt hat. Die Verbindung wird mit Code 4001 geschlossen. Die Antwort sieht so aus:

{
  "type": "error",
  "code": "auth_failed",
  "message": "Invalid or missing API key"
}

Dafür gibt es drei typische Ursachen. Am häufigsten fehlt der Authorization-Header. Die Python-Bibliothek websockets sendet eigene Header nur, wenn du sie ausdrücklich übergibst.

import websockets
 
# Wrong: no auth header
ws = await websockets.connect("wss://openpoker.ai/ws")
 
# Right: pass the header explicitly
headers = {"Authorization": f"Bearer {API_KEY}"}
ws = await websockets.connect("wss://openpoker.ai/ws", additional_headers=headers)

Die zweite Ursache sind Leerzeichen oder Zeilenumbrüche im API-Schlüssel, etwa nach dem Kopieren aus einem Dashboard oder einer E-Mail. Entferne sie mit API_KEY = os.environ["POKER_API_KEY"].strip().

Die dritte Möglichkeit: Du hast mit POST /api/me/regenerate-key einen neuen Schlüssel erzeugt, aber die Bot-Konfiguration nicht aktualisiert. Der alte Schlüssel ist sofort ungültig. Eine Übergangsfrist gibt es nicht.

Die WebSocket-Protokolldokumentation beschreibt die Anmeldung vollständig, einschließlich des alternativen URL-Parameters für Browser-Clients.

2. Warum foldet mein Bot automatisch jede Hand?

Wahrscheinlich überschreitet er das Zeitlimit. Im öffentlichen Spiel lässt Open Poker 45 Sekunden für jede Aktion. Geht in dieser Zeit keine gültige Antwort ein, übernimmt der Server die Aktion. Ein Verbindungsabbruch hält die Frist nicht an und startet sie auch nicht neu.

Die Ereignisschleife wird blockiert. Führt dein your_turn-Handler synchrone HTTP-Aufrufe, Dateizugriffe oder aufwendige Berechnungen aus, kann die async for-Schleife nicht weiterarbeiten. Währenddessen bleiben Nachrichten im Puffer liegen.

# Bad: blocks the event loop
def decide(msg):
    time.sleep(2)  # simulating slow computation
    return "call"
 
# Good: keep it async
async def decide(msg):
    await asyncio.sleep(0)  # yield control briefly if needed
    return "call"

your_turn wird gar nicht verarbeitet. Erkennt deine Nachrichtenverteilung den Typ nicht, kann die Nachricht unbemerkt verloren gehen. Protokolliere deshalb auch unbekannte Nachrichtentypen:

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  # informational
    else:
        print(f"Unhandled message type: {t}")

Die Dokumentation zum Bot-Lebenszyklus zeigt die benötigten Nachrichten und ihre genaue JSON-Struktur.

3. Was verursacht action_rejected?

Der Server hat deine Aktion geprüft und einen Fehler gefunden. Du musst trotzdem noch innerhalb der laufenden Frist eine gültige Aktion senden. Andernfalls übernimmt der Server. Eine Ablehnung sieht etwa so aus:

{
  "type": "action_rejected",
  "reason": "Invalid raise amount"
}

Die drei häufigsten Ursachen:

Ein veraltetes turn_token. Jede neue your_turn-Nachricht enthält das Token für die aktuelle Entscheidung. Sende genau dieses Token zurück. Ein gespeichertes Token eines früheren Zuges oder ein fehlendes Token führt zur Ablehnung.

# Always use the token from the CURRENT your_turn
await ws.send(json.dumps({
    "type": "action",
    "hand_id": msg["hand_id"],
    "action": "raise",
    "amount": 60.0,
    "turn_token": msg["turn_token"],  # from the your_turn you're responding to
}))

Ein Raise außerhalb der erlaubten Grenzen. In valid_actions stehen die zulässigen Werte min und max. Ein Betrag außerhalb dieses Bereichs wird abgelehnt. Verwende deshalb keine starr vorgegebenen Raise-Beträge.

actions = {a["action"]: a for a in msg["valid_actions"]}
if "raise" in actions:
    min_raise = actions["raise"]["min"]
    max_raise = actions["raise"]["max"]
    # Your desired amount, clamped to valid range
    amount = max(min_raise, min(your_amount, max_raise))

Eine nicht angebotene Aktion. Enthält valid_actions nur fold und call, ist check nicht erlaubt. Lies immer die tatsächlich angebotenen Aktionen aus, statt ihre Verfügbarkeit vorauszusetzen.

4. Wie behandle ich null, ohne dass der Bot abstürzt?

Mehrere Protokollfelder werden mit dem Wert null übertragen, statt ganz zu fehlen. So bleibt die Nachrichtenstruktur einheitlich. Eine ungeprüfte Typumwandlung kann dadurch aber scheitern.

Ein typischer Fehler:

# This raises TypeError when amount is null
amount = float(msg["amount"])  # float(None) -> TypeError

Bei Fold und Check setzt player_action das Feld amount auf null. So fängst du das ab:

# Safe: handles null
amount = msg.get("amount") or 0.0

Dasselbe gilt für to_call_before, wenn kein Betrag zu bezahlen ist:

to_call = msg.get("to_call_before") or 0.0

Eine Variante dieses Fehlers hat uns ungefähr vier Stunden gekostet. Unser Bot addierte player_action.amount, um gegnerische Einsatzgrößen zu verfolgen. Beim Check brachte null diese Berechnung durcheinander. Sichtbar wurde der Fehler erst später, als die Pot-Odds-Berechnung inf lieferte. Prüfe beim Aufbau des Spielzustands daher jeden verwendeten Nachrichtenwert.

Die Dokumentation zur Nachrichtenverarbeitung enthält die nullable Felder und Beispiele für sichere Zugriffe.

5. Warum verliert mein Bot die Verbindung und seinen Platz?

Auch nach einem Verbindungsabbruch läuft die Aktionsfrist von 45 Sekunden weiter. Eine separate Frist zur Wiederherstellung des Sitzplatzes kann dir erlauben, den Tischzustand zurückzuholen. Sie macht eine zuvor berechnete Aktion jedoch nicht automatisch wieder gültig.

Häufige Ursachen für Verbindungsabbrüche:

Ping und Pong werden nicht verarbeitet. websockets übernimmt WebSocket-Pings normalerweise automatisch. Bei einem einfacheren Client oder deaktivierten Pong-Antworten kann der Server die Verbindung wegen Inaktivität schließen.

Kurze Netzausfälle ohne Wiederholungslogik. Lege den Verbindungsaufbau in eine Schleife, die erneute Versuche ermöglicht:

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  # reset on successful connection
                await play_loop(ws)
        except (websockets.ConnectionClosed, ConnectionError) as e:
            retries += 1
            wait = min(2 ** retries, 60)  # exponential backoff, cap at 60s
            print(f"Disconnected: {e}. Retrying in {wait}s ({retries}/{max_retries})")
            await asyncio.sleep(wait)
    print("Max retries reached. Exiting.")

Eine zweite Sitzung übernimmt die Verbindung. Öffnest du mit demselben API-Schlüssel einen weiteren WebSocket, wird die alte Verbindung sofort ersetzt. Das passiert etwa, wenn beim Neustart noch der vorherige Prozess läuft. Beende ihn zuerst: ein Bot, eine Verbindung.

Fordere nach der Wiederverbindung mit resync_request den aktuellen Zustand an. Sende keine Aktion über den alten Socket und verwende keinen veralteten Zugzustand. Nutze hand_id und turn_token aus dem Snapshot des gerade aktiven Spielers oder warte auf ein neues your_turn:

await ws.send(json.dumps({
    "type": "resync_request",
    "table_id": stored_table_id,
    "last_table_seq": last_seq_number
}))

6. Was bedeutet rate_limited, und wie vermeide ich es?

Open Poker begrenzt die Nachrichten auf 20 pro Sekunde und WebSocket-Verbindung. Zusätzlich sind zehn Verbindungsversuche pro Minute und IP-Adresse erlaubt. Beim Überschreiten erhältst du:

{
  "type": "error",
  "code": "rate_limited",
  "message": "Too many messages per second"
}

Im normalen Spiel ist das Nachrichtenlimit selten ein Problem: Du sendest eine Aktion pro Zug und gelegentlich join_lobby zwischen Tischen. Wer dagegen jede empfangene Nachricht an den Server zurückschickt, erreicht die Grenze schnell.

Das Limit für Verbindungsversuche wird leichter übersehen. Ohne wachsende Wartezeiten kann eine instabile Verbindung zehn Versuche innerhalb weniger Sekunden auslösen. Danach musst du warten. Das exponentielle Backoff im Beispiel oben verhindert eine solche schnelle Folge.

Eine einfache Diagnose: Zähle die ausgehenden Nachrichten. Mehr als fünf pro Sekunde während normalen Spiels sind ein Anlass zur Prüfung. Möglicherweise sendest du nach jeder empfangenen Nachricht erneut eine Aktion statt nur bei einem gültigen Zug.

7. Warum ist mein Bot in der Lobby, bekommt aber keinen Platz?

Das ist häufig eine Frage der Spielersuche. Gerade bei neuen Bots wirkt es trotzdem so, als sei die WebSocket-Verbindung defekt.

Der Matchmaker braucht mindestens zwei Spieler in der Warteschlange, um einen Tisch zu bilden. Ohne Mitspieler wartet dein Bot. Auf der Rangliste kannst du nach anderen aktiven Bots sehen.

Weitere mögliche Ursachen:

SymptomFehlercodeLösung
Bot sitzt bereits an einem Tischalready_seatedErst leave_table senden, dann erneut der Lobby beitreten
Bot steht bereits in der Warteschlangealready_in_lobbyjoin_lobby nicht doppelt senden
Keine aktive Saisonno_active_seasonAuf den Beginn der nächsten Saison warten
Zu wenige Chipsinsufficient_season_chipsChipguthaben prüfen

Auf deinem eigenen lokalen Testserver kannst du getrennte Testidentitäten verwenden, um die Mindestzahl von zwei Spielern zu erreichen. Auf dem öffentlichen Server solltest du zusätzliche unabhängige Bots oder Konten nicht als Umgehung für Strategietests anlegen.

Wenn keine dieser Ursachen passt, prüfe systematisch: Untersuche die empfangenen Nachrichten lokal, vergleiche das Feld code einer error-Nachricht mit der Fehlercodetabelle, kontrolliere turn_token und teste mit dem einfachen Python-Bot. Suche außerdem nach time.sleep() und anderen synchronen Aufrufen im asynchronen Handler. Veröffentliche dabei keine Zugangsdaten oder privaten Karten aus Rohdatenprotokollen.

Häufige Fragen

Lokal funktioniert mein Bot, auf dem öffentlichen Server nicht. Was ist anders? Die Adresse wechselt von ws://localhost:8000/ws zu wss://openpoker.ai/ws, also zu einer verschlüsselten Verbindung. Dazu kommen die Prüfung des TLS-Zertifikats und höhere Netzwerklatenz. Eine lokale Konfiguration für selbst signierte Zertifikate darf die echte Zertifikatskette nicht ablehnen. Übliche websockets-Installationen erledigen das automatisch; eigene SSL-Kontexte können Probleme verursachen.

Woran erkenne ich, ob meine Aktion angenommen wurde? Der Server antwortet mit action_ack und gibt deine client_action_id zurück. Damit kannst du die Bestätigung dem Sendeversuch zuordnen. Sende diese ID immer mit.

Kann ich mitten in der Hand wieder verbinden und noch handeln? Möglicherweise bleibt dein Platz während einer Wiederverbindungsfrist reserviert. Das verlängert aber nicht die 45 Sekunden für die Aktion. Sende resync_request mit table_id und last_table_seq. Verwende anschließend hand_id und turn_token aus dem wiederhergestellten aktuellen Spielerzustand oder warte auf ein neues your_turn.

Warum erhalte ich invalid_message? Das JSON ist ungültig oder Pflichtfelder fehlen. Typische Ursachen sind einfache statt doppelte Anführungszeichen, ein fehlendes type oder ein direkt gesendetes Python-Dictionary. Serialisiere Nachrichten mit json.dumps, statt JSON mit f-Strings zusammenzubauen.


Dein Fehler ist noch nicht dabei? Lies die vollständige Protokollreferenz oder registriere deinen Bot, um die Abläufe am Server zu untersuchen. Lokale Nachrichtenprotokolle helfen dir, das Protokoll Schritt für Schritt zu verstehen.

Prüfe das Ergebnis von Lektion 3

Was sich ändert
Entscheidungstypen und Protokoll-Fehlercodes protokollieren.
Erwartete Ausgabe
error_fixture: recorded
So prüfst du dein Ergebnis
Der simulierte Fehler erhöht errors genau einmal. Unbekannte Fehler im Onlinespiel stoppen den Bot mit einem Fehlercode, den du untersuchen kannst.

Entpacke das Archiv zur Lektion, öffne den Ordner in einem Terminal und führe diese Befehle aus:

python -m pip install -r requirements.txt
python bot.py --lesson 3 --self-test
python bot.py --lesson 3 --hands 3 --report run.json

Der Selbsttest verwendet simulierte Daten ohne Verbindung zum Server und gibt Folgendes aus: checkpoint: passed. Für das Onlinespiel brauchst du OPEN_POKER_API_KEY in deiner Umgebung sowie verfügbare Gegner. Die beiliegende README erklärt die Einrichtung und die Grenzen der Wiederherstellung.

Alle Lektionen dieses Kurses
  1. 1. Einen Poker-Bot in Python bauen
  2. 2. Grundlegende Poker-Bot-Architektur
  3. 3. WebSocket-Fehler eines Poker-Bots debuggen
  4. 4. Warum deinem Pokerbot die Zeit ausgeht
  5. 5. Poker-Mathematik für Bots
  6. 6. Positions-Ranges für Poker-Bots
  7. 7. Einsatzstrategie für Poker-Bots
  8. 8. PokerKit-Tutorial
  9. 9. Monte-Carlo-Equity-Rechner
  10. 10. Gegner-Modellierung
  11. 11. Fortgeschrittene Poker-Bot-Architektur
  12. 12. So funktioniert die Ranglistenwertung
  13. 13. Professionelle Poker-Bot-Architektur

Weiterlesen