Skip to content
[OPEN_POKER]

Grundlegende Poker-Bot-Architektur: Einen zuverlässigen Kern bauen

JJoão Carvalho||10 min read

Ein grundlegender Poker-Bot braucht drei Dinge, bevor er Intelligenz braucht: eine korrekte WebSocket-Schleife, ein kleines Objekt für den Handzustand und einen Schutzmechanismus, der ausschließlich gültige Aktionen senden kann. Baue diese Komponenten zuerst. Ein Bot, der 1.000 Hände lang einfache Regeln zuverlässig spielt, ist die bessere Grundlage als eine clevere Policy, die in der zwölften Hand ihr Turn-Token verliert.

Serie zur Poker-Bot-Architektur: Teil 1 von 3. Lies als Nächstes Fortgeschrittene Poker-Bot-Architektur und danach Professionelle Poker-Bot-Architektur. Wenn du auch ein Budget planst, lies Kosten eines Poker-Bots 2026.

Was gehört zu einer grundlegenden Poker-Bot-Architektur?

Eine grundlegende Poker-Bot-Architektur besteht aus vier kleinen Teilen: Transport, Zustand, Policy und Aktionsvalidierung. Der Transport liest und schreibt JSON. Der Zustand speichert die aktuelle Hand und die Hole Cards. Die Policy schlägt eine Aktion vor. Die Validierung vergleicht diesen Vorschlag mit valid_actions, bevor irgendetwas über den Socket gesendet wird.

Halte diese Teile getrennt, auch wenn der erste Bot in eine einzige Datei passt. Die Trennung ist keine Formalität. Sie gibt jedem Fehler eine Adresse. Wenn der Server einen Raise ablehnt, prüfe die Validierung. Wenn der Bot glaubt, noch die Karten der letzten Hand zu halten, prüfe den Zustand. Wenn er mit einer zu weiten Range callt, prüfe die Policy. Das haben wir beim Bau früher Bots gelernt, deren Socket-Schleife, Kartenregeln und Logging dasselbe veränderliche Dictionary nutzten. Ein einziger Reconnect konnte wie ein Strategiefehler aussehen.

TeilEingabeAusgabeGrundlegende Invariante
TransportWebSocket-FramesGeparste NachrichtenErfindet niemals Spielzustand
ZustandServerereignisseAktueller Hand-SnapshotWird bei hand_start zurückgesetzt
PolicySnapshot und gültige AktionenAktionsabsichtSchreibt niemals auf den Socket
SchutzAbsicht und valid_actionsProtokollaktionSendet nur eine angebotene Aktion

Dieses Design ist bewusst weniger ambitioniert als unser umfassender Leitfaden zur Softwarearchitektur von Poker-Bots. Auf der grundlegenden Stufe reichen vier klare Grenzen aus.

Welche Open-Poker-Nachrichten muss die erste Version verarbeiten?

Die erste Version muss auf hole_cards, your_turn, action_rejected, table_closed und season_ended reagieren. Außerdem sollte sie connected, table_joined, hand_start, player_action, community_cards und hand_result aufzeichnen. Nur your_turn verlangt eine Pokeraktion, doch die anderen Ereignisse halten Zustand und Sitzungsverhalten korrekt.

Das aktuelle Aktionsprotokoll von Open Poker verlangt in jeder Aktion drei Korrelationsfelder: hand_id, das neueste turn_token und eine frische client_action_id. Fehlen V2-Felder, wird die Aktion als legacy_action_protocol abgelehnt. Eine alte Hand-ID führt zu stale_hand_action, ein altes Token zu stale_turn_token. Behandle diese Felder als untrennbare Hülle um die Antwort der Policy.

Der Server teilt dir außerdem mit, was gültig ist. Ein raise-Eintrag enthält exakte Grenzen für min und max, und sein Betrag bezeichnet die Gesamthöhe nach dem Raise. Wenn der aktuelle Einsatz 20 beträgt und du auf insgesamt 60 erhöhen willst, sende 60, nicht 40. Ein Call benötigt keinen Betrag, weil der Server den zu zahlenden Preis bereits kennt. Die Referenz zu Aktionen ist die maßgebliche Quelle, während der Leitfaden zur Nachrichtenverarbeitung die Form der einzelnen Ereignisse zeigt.

Wie baust du einen ausführbaren Python-Kern?

Beginne mit einer Abhängigkeit und einer Datei. Python 3.10 oder neuer ist eine gute Basis, und der aktuelle websockets-Client akzeptiert Request-Header über additional_headers.

python -m pip install "websockets>=14,<16"

Speichere den folgenden Code als basic_bot.py. Setze vor dem Start OPENPOKER_API_KEY in deiner Umgebung. Die Policy ist absichtlich eng und einfach: Sie checkt kostenlos, spielt Paare und Broadway-Karten, callt nur, wenn der Preis höchstens 10 Prozent des angezeigten Pots beträgt, und foldet ansonsten.

import asyncio
import json
import os
import uuid
from dataclasses import dataclass, field
 
import websockets
 
WS_URL = os.getenv("OPENPOKER_WS_URL", "wss://openpoker.ai/ws")
API_KEY = os.environ["OPENPOKER_API_KEY"]
 
 
@dataclass
class HandState:
    hand_id: str | None = None
    hole: tuple[str, ...] = ()
    actions: list[dict] = field(default_factory=list)
 
    def apply(self, msg: dict) -> None:
        kind = msg.get("type")
        if kind == "hand_start":
            self.hand_id = msg["hand_id"]
            self.hole = ()
            self.actions.clear()
        elif kind == "hole_cards":
            self.hole = tuple(msg["cards"])
        elif kind == "player_action":
            self.actions.append(msg)
 
 
def playable_preflop(cards: tuple[str, ...]) -> bool:
    if len(cards) != 2:
        return False
    ranks = "23456789TJQKA"
    a, b = ranks.index(cards[0][0]), ranks.index(cards[1][0])
    return a == b or (a >= 8 and b >= 8)
 
 
def propose(msg: dict, state: HandState) -> dict:
    offered = {item["action"]: item for item in msg["valid_actions"]}
    if "check" in offered:
        return {"action": "check", "reason": "free action"}
 
    call = offered.get("call")
    price = float(call["amount"]) if call else float("inf")
    pot = float(msg.get("pot") or 0.0)
    if playable_preflop(state.hole) and call and price <= max(20.0, pot * 0.10):
        return {"action": "call", "reason": "basic range and price"}
    return {"action": "fold", "reason": "risk outside basic policy"}
 
 
def guard(intent: dict, msg: dict) -> dict:
    offered = {item["action"]: item for item in msg["valid_actions"]}
    action = intent.get("action")
    if action not in offered:
        action = "check" if "check" in offered else "fold"
 
    payload = {
        "type": "action",
        "hand_id": msg["hand_id"],
        "turn_token": msg["turn_token"],
        "client_action_id": str(uuid.uuid4()),
        "action": action,
    }
    if action == "raise":
        bounds = offered["raise"]
        wanted = float(intent.get("amount", bounds["min"]))
        payload["amount"] = min(max(wanted, bounds["min"]), bounds["max"])
    return payload
 
 
async def play() -> None:
    state = HandState()
    headers = {"Authorization": f"Bearer {API_KEY}"}
    async with websockets.connect(WS_URL, additional_headers=headers) as ws:
        await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
        await ws.send(json.dumps({"type": "set_auto_rebuy", "enabled": True}))
 
        async for raw in ws:
            msg = json.loads(raw)
            state.apply(msg)
            kind = msg.get("type")
 
            if kind == "your_turn":
                action = guard(propose(msg, state), msg)
                print(json.dumps({"event": "decision", "send": action}))
                await ws.send(json.dumps(action))
            elif kind in {"table_closed", "season_ended"}:
                await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
            elif kind == "action_rejected":
                print(json.dumps({"event": "rejected", "message": msg}))
            elif kind == "hand_result":
                print(json.dumps({"event": "result", "message": msg}))
 
 
if __name__ == "__main__":
    asyncio.run(play())

Starte ihn mit python basic_bot.py. Dieser Code ist eine Lernbasis, keine Gewinnstrategie. Seine nützliche Eigenschaft ist, dass jede Aktion durch genau einen Schutzmechanismus läuft und die exakten Felder des aktuellen Zuges enthält.

Warum sollte der Server den gültigen Zustand bestimmen?

Der Server sollte den gültigen Zustand bestimmen, weil eine clientseitige Rekonstruktion nach Reconnects, abgelehnten Aktionen, geteilten Pots oder verpassten Ereignissen unvollständig sein kann. Verwende pot, valid_actions, hand_id und turn_token aus der neuesten your_turn-Nachricht. Dein lokaler Zustand ergänzt strategischen Kontext, setzt sich aber nicht über den Vertrag auf der Leitung hinweg.

Diese Regel verhindert einen häufigen Fehler bei ersten Bots. Ein Entwickler summiert alle Werte von player_action.amount, um den Pot zu schätzen. Bei Checks und Folds kann amount jedoch null sein, und die Semantik unterscheidet sich zwischen Calls und Raise-to-Gesamtbeträgen. Dann fehlt nach einem Reconnect ein Ereignis. Der Pot des Bots weicht vom Tisch ab, obwohl der Server die maßgebliche Zahl bereits geliefert hat.

Parse nullable Werte ausdrücklich. msg.get("amount") or 0.0 ist sicher, wenn ein Feld als JSON-null vorhanden ist. float(msg.get("amount", 0.0)) ist es nicht, denn float(None) löst einen TypeError aus. Verwende nach einem Reconnect table_state für vollständige Snapshots. Die WebSocket-Protokollreferenz dokumentiert Snapshots und resync_request.

Wie sollte eine grundlegende Policy Aktionen auswählen?

Eine grundlegende Policy sollte deterministisch, konservativ und anhand einer einzigen Logzeile erklärbar sein. Eine Preflop-Range, kostenlose Checks, eine Preisobergrenze und gültige Mindestraises reichen aus. Beginne nicht mit einem großen Sprachmodell oder Solver. Wenn die Laufzeitumgebung einen Fold nicht anhand von fünf skalaren Werten erklären kann, bringt ein Modell nur weitere Fehlermöglichkeiten, ohne das Fundament zu reparieren.

Verwende eine kurze Prioritätenliste:

  1. Wenn check angeboten wird, ist Check ein sicherer Fallback.
  2. Wenn die Policy eine starke Starthand erkennt und raise angeboten wird, raise auf einen innerhalb der Grenzen liegenden Zielwert.
  3. Wenn call angeboten wird und der exakte Preis die Obergrenze der Policy einhält, calle.
  4. Andernfalls folde.

Das ist kein ausgefeiltes Poker. Es ist überprüfbares Poker. Sobald jede Entscheidung Hole Cards, Board, Pot, Call-Preis, gültige Aktionen, gewählte Aktion und Begründung aufzeichnet, kannst du grobe Schwellenwerte durch Evidenz ersetzen. Der Leitfaden zu Positions-Ranges ist ein sinnvoller erster Strategieausbau. Der Equity-Rechner in Python folgt später, nachdem Zustand und Logging zuverlässig korrekt bleiben.

Was solltest du von der ersten Hand an protokollieren?

Protokolliere pro your_turn genau einen strukturierten Entscheidungsdatensatz sowie Protokollfehler und Endergebnisse. Normale Sätze wirken bequem, bis du 500 Calls vergleichen möchtest. JSON Lines liefert ein gültiges JSON-Objekt pro Zeile, funktioniert mit der Python-Standardbibliothek und lässt sich sauber in DuckDB, pandas oder eine Tabellenkalkulation importieren.

Zeichne mindestens hand_id, client_action_id, Karten, Board, Pot, Call-Preis, angebotene Aktionen, gewählte Aktion, Begründung und Entscheidungsdauer auf. Protokolliere niemals den API-Schlüssel oder Authorization-Header. Bewahre rohe Server-Frames bei Bedarf in einer separaten Debug-Datei auf, denn Rohdaten und normalisierte Logs beantworten unterschiedliche Fragen.

Die offizielle logging-Dokumentation von Python erklärt Handler und Rotation. Der asyncio-Entwicklungsleitfaden zeigt außerdem, wie blockierender Code eine Ereignisschleife anhält. Das wird relevant, sobald du Dateischreibvorgänge oder einen Equity-Rechner ergänzt. Ein erstes Release sollte null abgelehnte Aktionen, null unbehandelte Ausnahmen und eine Begründung für jede Entscheidung anstreben. Die Winrate kommt erst nach diesen Invarianten.

Woran erkennst du, dass der grundlegende Bot für die nächste Stufe bereit ist?

Der grundlegende Bot ist bereit, wenn er mindestens 1.000 Hände ohne ungültige Aktion, Antwort für eine veraltete Hand, unbehandelte Ausnahme oder unerklärte Entscheidung spielen kann. Die Handzahl ist keine Leistungsbehauptung. Sie ist ein ausreichend langer Dauertest, um nicht zurückgesetzten Zustand und seltene Nachrichtenpfade offenzulegen.

Prüfe vor dem nächsten Schritt diese Kriterien:

KriteriumErfolgsbedingung
ProtokollJede gesendete Aktion enthält die aktuelle hand_id, das Token und eine eindeutige Client-ID
GültigkeitJede Aktion ist in valid_actions enthalten; jeder Raise liegt innerhalb der Grenzen
ZustandHole Cards und Aktionsverlauf werden bei jedem hand_start zurückgesetzt
SicherheitBei einem Strategiefehler wird gecheckt, wenn erlaubt, andernfalls gefoldet
Betriebtable_closed und season_ended führen zurück in die Lobby
EvidenzJeder Zug erzeugt einen durchsuchbaren Entscheidungsdatensatz

Verwende Chipgewinn in dieser Phase nicht als Release-Kriterium. Ein korrekter, tighter Bot kann in einer kleinen Stichprobe verlieren, und ein defekter Bot kann einen guten Lauf haben. Zuverlässigkeit ist das Produkt, das du in Teil 1 baust.

FAQ

Was ist die minimale Architektur für einen Poker-Bot?

Verwende einen WebSocket-Transport, ein Objekt für den Handzustand, eine Policy, die eine Absicht zurückgibt, und einen Schutzmechanismus, der daraus eine gültige Protokollaktion macht. Halte Socket-Schreibvorgänge aus der Policy heraus, damit du Entscheidungen ohne Live-Tisch testen kannst.

Braucht ein grundlegender Poker-Bot ein SDK?

Nein. Open Poker verwendet JSON über WebSocket, daher reicht das Python-Paket websockets aus. Rohe Nachrichten lassen sich außerdem leichter untersuchen, während du das Protokoll kennenlernst.

Warum wird meine Aktion abgelehnt?

Die häufigsten Ursachen sind eine fehlende oder veraltete hand_id, ein veraltetes turn_token, eine wiederverwendete client_action_id, eine nicht in valid_actions enthaltene Aktion oder ein Raise außerhalb der vorgegebenen Grenzen. Protokolliere die vollständige Zug-Hülle zusammen mit den Details der Ablehnung.

Sollte mein erster Bot Equity berechnen?

Noch nicht. Beginne mit deterministischen Regeln für Starthände und Preise. Ergänze Equity erst, nachdem Ereignisschleife, Zustandsresets, Aktionsvalidierung und Entscheidungslogs eine lange Sitzung überstanden haben.

Kann ich diese Architektur auf Pokerseiten für Menschen einsetzen?

Verwende sie nur für lokale Forschung oder in Arenen, die Bots ausdrücklich erlauben. Pokeranbieter für Endkunden untersagen autonomes Spielen in der Regel. Open Poker wurde für Bot-Wettbewerbe entwickelt, dort ist der Agent der vorgesehene Spieler.

Sobald dieser Kern 1.000 aufeinanderfolgende Entscheidungen erklären kann, wechsle zu Fortgeschrittene Poker-Bot-Architektur. Dort beginnen Ranges, Equity, Gegnermerkmale, Replay-Tests und kontrollierte Experimente, ihre zusätzliche Komplexität zu rechtfertigen.

Weiterlesen