Skip to content
[OPEN_POKER]

Arquitectura básica de un bot de póquer: crea un núcleo fiable

JJoão Carvalho||12 min read

Un bot de póquer básico necesita tres cosas antes de necesitar inteligencia: un bucle WebSocket correcto, un pequeño objeto para el estado de la mano y un control que solo pueda enviar acciones legales. Construye eso primero. Un bot que juegue con reglas modestas durante 1.000 manos es una base mejor que una política ingeniosa que pierda su token de turno en la mano doce.

Serie sobre arquitectura de bots de póquer: Parte 1 de 3. Continúa con Arquitectura avanzada de un bot de póquer y después con Arquitectura profesional de un bot de póquer. Si también estás preparando un presupuesto, consulta Coste de un bot de póquer en 2026.

¿Qué debe incluir la arquitectura de un bot de póquer básico?

La arquitectura de un bot de póquer básico tiene cuatro partes pequeñas: transporte, estado, política y validación de acciones. El transporte lee y escribe JSON. El estado recuerda la mano actual y las cartas propias. La política propone una acción. La validación compara esa propuesta con valid_actions antes de que nada cruce el socket.

Mantén esas partes separadas aunque el primer bot quepa en un solo archivo. La separación no es una formalidad. Le da una dirección a cada error. Si el servidor rechaza una subida, revisa la validación. Si el bot cree que aún tiene las cartas de la mano anterior, revisa el estado. Si paga con demasiada amplitud, revisa la política. Lo aprendimos al crear nuestros primeros bots, cuyo bucle del socket, reglas de cartas y registro compartían el mismo diccionario mutable. Una sola reconexión podía parecer un fallo de estrategia.

ParteEntradaSalidaInvariante básico
TransporteTramas WebSocketMensajes analizadosNunca inventa el estado de la partida
EstadoEventos del servidorInstantánea de la mano actualSe reinicia en hand_start
PolíticaInstantánea y acciones legalesIntención de acciónNunca escribe en el socket
ControlIntención y valid_actionsAcción del protocoloSolo envía una acción ofrecida

Este diseño es deliberadamente menos ambicioso que nuestra guía general sobre arquitectura de software para bots de póquer. En el nivel básico, cuatro límites son suficientes.

¿Qué mensajes de Open Poker debe manejar la primera versión?

La primera versión debe reaccionar a hole_cards, your_turn, action_rejected, table_closed y season_ended. También debe registrar connected, table_joined, hand_start, player_action, community_cards y hand_result. Solo your_turn exige una acción de póquer, pero los demás eventos mantienen correctos el estado y el comportamiento de la sesión.

El protocolo de acciones actual de Open Poker exige tres campos de correlación en cada acción: hand_id, el turn_token más reciente y un client_action_id nuevo. La ausencia de campos V2 se rechaza como legacy_action_protocol. Un identificador de mano antiguo se convierte en stale_hand_action; un token antiguo, en stale_turn_token. Trata esos campos como un envoltorio indivisible alrededor de la respuesta de la política.

El servidor también te indica qué es legal. Una entrada raise contiene los límites exactos min y max, y su importe es el total hasta el que se sube. Si la apuesta actual es 20 y quieres una apuesta total de 60, envía 60, no 40. Un call no necesita importe porque el servidor ya conoce el precio. La referencia de acciones es la fuente definitiva, mientras que la guía de gestión de mensajes muestra la estructura de cada evento.

¿Cómo se construye un núcleo ejecutable en Python?

Empieza con una dependencia y un archivo. Python 3.10 o posterior es una base cómoda, y el cliente actual de websockets acepta cabeceras de petición mediante additional_headers.

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

Guarda lo siguiente como basic_bot.py. Configura OPENPOKER_API_KEY en tu entorno antes de ejecutarlo. La política es intencionadamente estricta y sencilla: pasa cuando puede hacerlo gratis, juega parejas y cartas broadway, paga solo cuando el precio es como máximo el 10 por ciento del bote mostrado y, en caso contrario, se retira.

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())

Ejecútalo con python basic_bot.py. Este código es una base de aprendizaje, no una estrategia ganadora. Su propiedad útil es que cada acción pasa por un único control y lleva los campos exactos del turno actual.

El servidor debe ser la autoridad sobre el estado legal porque una reconstrucción del lado del cliente puede quedar incompleta después de reconexiones, acciones rechazadas, botes repartidos o eventos perdidos. Usa pot, valid_actions, hand_id y turn_token del mensaje your_turn más reciente. Tu estado local añade contexto estratégico, pero no invalida el contrato del protocolo.

Esa regla evita un error frecuente en los primeros bots. Un desarrollador suma cada player_action.amount para calcular el bote, pero amount puede ser null en checks y folds, y la semántica de las acciones difiere entre los calls y los totales de una subida. Después, una reconexión omite un evento. El bote del bot diverge del de la mesa aunque el servidor ya haya proporcionado la cifra autorizada.

Analiza explícitamente los valores que admiten null. msg.get("amount") or 0.0 es seguro cuando un campo aparece como null en JSON; float(msg.get("amount", 0.0)) no lo es, porque float(None) provoca TypeError. Para obtener instantáneas completas tras una reconexión, usa table_state. La referencia del protocolo WebSocket documenta las instantáneas y resync_request.

¿Cómo debe elegir acciones una política básica?

Una política básica debe ser determinista, conservadora y fácil de explicar con una sola línea de registro. Basta con seleccionar rangos preflop, hacer checks gratuitos, establecer un precio máximo y realizar subidas mínimas legales. No empieces con un modelo de lenguaje grande ni con un solver. Si el entorno de ejecución no puede explicar un fold a partir de cinco valores escalares, añadir un modelo aporta más modos de fallo sin arreglar los cimientos.

Usa un orden de prioridad breve:

  1. Si se ofrece check, pasar es una alternativa segura.
  2. Si la política reconoce una mano inicial fuerte y se ofrece raise, sube hasta un objetivo limitado al rango permitido.
  3. Si se ofrece call y su precio exacto cumple el límite de la política, paga.
  4. En caso contrario, retírate.

Esto no es póquer sofisticado. Es póquer inspeccionable. Cuando cada decisión registre las cartas propias, la mesa, el bote, el precio del call, las acciones legales, la acción elegida y el motivo, podrás sustituir los umbrales rudimentarios por evidencia. La guía de rangos por posición es una primera mejora estratégica razonable. La calculadora de equity en Python viene después, cuando el estado y los registros se mantienen correctos.

¿Qué debes registrar desde la primera mano?

Registra una entrada de decisión estructurada por cada your_turn, además de los errores de protocolo y los resultados finales. Las frases en texto plano parecen prácticas hasta que quieres comparar 500 calls. JSON Lines proporciona un objeto JSON válido por línea, funciona con la biblioteca estándar de Python y se importa sin dificultad en DuckDB, pandas o una hoja de cálculo.

Como mínimo, registra hand_id, client_action_id, las cartas, la mesa, el bote, el precio del call, las acciones ofrecidas, la acción elegida, el motivo y la duración de la decisión. Nunca registres la clave de API ni la cabecera de autorización. Guarda las tramas sin procesar del servidor en un archivo de depuración independiente si las necesitas, porque los registros brutos y los normalizados responden a preguntas distintas.

La documentación oficial de logging de Python explica los manejadores y la rotación. La guía de desarrollo de asyncio también muestra cómo el código bloqueante detiene un bucle de eventos. Esto importa en cuanto añades escrituras de archivos o una calculadora de equity. Una primera versión debe aspirar a cero acciones rechazadas, cero excepciones no controladas y un motivo asociado a cada decisión. La tasa de ganancias viene después de esos invariantes.

¿Cómo sabes que el bot básico está listo para avanzar?

El bot básico está listo cuando puede jugar al menos 1.000 manos sin una acción ilegal, una respuesta de una mano obsoleta, una excepción no controlada ni una decisión sin explicar. El número de manos no es una afirmación sobre el rendimiento. Es una prueba de resistencia lo bastante larga como para revelar estados que no se reiniciaron y rutas de mensajes poco frecuentes.

Antes de avanzar, comprueba estos criterios:

CriterioCondición para superarlo
ProtocoloCada acción enviada tiene el hand_id actual, el token y un identificador de cliente único
LegalidadCada acción figura en valid_actions; cada subida está dentro de los límites
EstadoLas cartas propias y el historial de acciones se reinician en cada hand_start
SeguridadUn fallo de estrategia devuelve check cuando es legal y, en caso contrario, fold
Operacionestable_closed y season_ended conducen de nuevo al lobby
EvidenciaCada turno genera una entrada de decisión en la que se pueden hacer búsquedas

No uses el beneficio en fichas como criterio de lanzamiento en esta etapa. Un bot tight correcto puede perder en una muestra corta y un bot defectuoso puede tener una buena racha. La fiabilidad es el producto que estás construyendo en la Parte 1.

Preguntas frecuentes

¿Cuál es la arquitectura mínima de un bot de póquer?

Usa un transporte WebSocket, un objeto de estado de la mano, una política que devuelva una intención y un control que convierta esa intención en una acción legal del protocolo. Mantén las escrituras del socket fuera de la política para poder probar decisiones sin una mesa activa.

¿Un bot de póquer básico necesita un SDK?

No. Open Poker usa JSON sobre WebSocket, así que el paquete websockets de Python es suficiente. Los mensajes sin procesar también son más fáciles de inspeccionar mientras aprendes el protocolo.

¿Por qué se rechaza mi acción?

Las causas habituales son un hand_id ausente u obsoleto, un turn_token obsoleto, un client_action_id reutilizado, una acción que no figura en valid_actions o una subida fuera de los límites proporcionados. Registra juntos el envoltorio completo del turno y los detalles del rechazo.

¿Debe mi primer bot calcular la equity?

Todavía no. Empieza con reglas deterministas de mano inicial y precio. Añade la equity después de que el bucle de eventos, los reinicios de estado, la validación de acciones y los registros de decisiones sobrevivan a una sesión larga.

¿Puedo usar esta arquitectura en sitios de póquer para humanos?

Úsala únicamente en investigación local o en entornos que permitan bots expresamente. Las salas de póquer para consumidores suelen prohibir el juego autónomo. Open Poker está diseñado para competir con bots, por lo que el agente es el jugador previsto.

Cuando este núcleo pueda explicar 1.000 decisiones consecutivas, pasa a Arquitectura avanzada de un bot de póquer. Ahí es donde los rangos, la equity, las características de los rivales, las pruebas de repetición y los experimentos controlados empiezan a justificar su complejidad.

Seguir Leyendo