Ir al contenido
[OPEN_POKER]

Usa Claude o GPT-4 como el Cerebro de tu Bot de Póker (Código Funcional)

JJoão Carvalho||Actualizado |11 min de lectura

Puedes conectar Claude o GPT-4 a un bot de póker en unas 80 líneas de Python. El LLM lee el mensaje your_turn, decide qué hacer, y tu bot ejecuta la acción. Cuesta aproximadamente $0.30 por 100 manos con Claude Haiku, toma decisiones en 600-900ms, y le gana a un calling station fácilmente. No le ganará a un bot heurístico afinado, pero es el camino más rápido hacia un motor de decisión funcional.

¿Por qué usar un LLM como motor de decisión de tu bot de póker?

Tres razones, en orden de importancia.

Velocidad de iteración. Un bot heurístico toma semanas para afinar: rangos preflop, sizing postflop, ajustes de posición y modelado de oponentes. Un bot LLM toma un solo prompt. Tu ciclo de iteración es «editar texto, reiniciar el bot», no «editar código, desplegarlo, recopilar datos y repetir». Para el desarrollo inicial, eso acelera el ciclo 10 veces.

Describir situaciones nuevas con lenguaje natural. Algunas situaciones poco frecuentes son difíciles de cubrir con reglas: por ejemplo, dos cartas consecutivas del mismo palo en un bote con varios jugadores, dos rivales que pagan y una mesa emparejada en el turn. Un LLM puede recurrir a conceptos de póker aprendidos para proponer una decisión en situaciones que tu código no había previsto.

Mejora de baseline gratuita. Los LLMs modernos están entrenados con suficiente estrategia de poker para jugar a nivel "intermedio competente" directo de fábrica. No necesitas enseñarle a Claude qué son pot odds. No necesitas explicar posición. El modelo ya sabe. Estás pagando $0.003 por decisión por el trabajo estratégico de alguien más.

La desventaja: los LLMs son lentos (600-1500ms por decisión), caros a escala ($0.30-$3.00 por 100 manos según el modelo), y no tan afilados como un bot heurístico bien afinado. Úsalos como punto de partida, no como punto final.

¿Cuál es el setup mínimo de un bot LLM?

Tres piezas: una conexión WebSocket con Open Poker, un cliente de API LLM, y un prompt que transforma el mensaje your_turn en una pregunta que el modelo puede responder.

Instala las dependencias:

pip install websockets anthropic

Configura dos variables de entorno: OPEN_POKER_API_KEY para la autenticación WebSocket y ANTHROPIC_API_KEY para Claude. El bot completo:

import asyncio
import json
import os
import websockets
from anthropic import AsyncAnthropic
 
API_KEY = os.environ["OPEN_POKER_API_KEY"]
WS_URL = "wss://openpoker.ai/ws"
client = AsyncAnthropic()
 
PROMPT = """You are playing 6-max No-Limit Hold'em at 10/20 blinds.
Decide what action to take based on the game state below.
 
Your hole cards: {hole_cards}
Community cards: {community_cards}
Pot size: {pot}
Your stack: {my_stack}
Your current bet: {my_bet}
Position (0=BTN, 1=SB, 2=BB, 3=UTG, etc): {seat}
Valid actions: {valid_actions}
 
Respond with ONLY a JSON object: {{"action": "fold|check|call|raise|all_in", "amount": <int or 0>}}
For raise, amount is the raise-to total (not increment). For check/call/fold, amount is 0.
"""
 
async def decide_action(state, hole_cards):
    prompt = PROMPT.format(
        hole_cards=hole_cards or "unknown",
        community_cards=state.get("community_cards", []),
        pot=state.get("pot", 0),
        my_stack=state.get("my_stack", 0),
        my_bet=state.get("my_bet", 0),
        seat=state.get("seat", -1),
        valid_actions=state.get("valid_actions", []),
    )
    msg = await client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=100,
        messages=[{"role": "user", "content": prompt}],
    )
    text = msg.content[0].text.strip()
    return json.loads(text)
 
async def play():
    headers = {"Authorization": f"Bearer {API_KEY}"}
    hole = None
    async with websockets.connect(WS_URL, additional_headers=headers) as ws:
        await ws.send(json.dumps({"type": "set_auto_rebuy", "enabled": True}))
        await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
 
        async for raw in ws:
            msg = json.loads(raw)
            t = msg.get("type")
 
            if t == "hole_cards":
                hole = msg["cards"]
            elif t == "your_turn":
                decision = await decide_action(msg, hole)
                await ws.send(json.dumps({
                    "type": "action",
                    "action": decision["action"],
                    "amount": decision.get("amount", 0),
                    "client_action_id": f"a-{msg['turn_token'][:8]}",
                    "turn_token": msg["turn_token"],
                }))
            elif t in ("table_closed", "season_ended"):
                await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
 
asyncio.run(play())

Ese es el bot completo. Guárdalo como llm_bot.py, configura tus dos API keys, ejecuta python llm_bot.py. Se conecta, se une a una mesa, y juega lo que Claude decida.

¿Qué LLM elegir?

El tradeoff es latencia, costo y habilidad. Tres opciones razonables:

ModeloCosto por 100 manosLatencia medianaNivel
Claude Haiku 4.5~$0.30600msIntermedio sólido
Claude Sonnet 4.5~$1.50900msFuerte, maneja edge cases
GPT-4o-mini~$0.40700msComparable a Haiku

Los números son estimaciones aproximadas de correr cada modelo por varios cientos de manos. El costo depende del largo del prompt y con qué frecuencia recortas el input. La latencia depende de la carga del proveedor.

Para un primer bot, usa Claude Haiku 4.5. Es rápido, barato, y juega suficientemente bien para ganarle al baseline de calling station. Puedes cambiar a Sonnet después si quieres juego más fuerte y no te importa el aumento de costo.

En las partidas públicas de Open Poker, el plazo de acción es de 45 segundos. Una llamada lenta al modelo puede consumir ese tiempo: limita la espera y prepara una acción alternativa rápida y permitida. Reconectarse no reinicia el plazo. Consulta la documentación de timeouts para conocer el comportamiento del servidor.

¿Cómo escribir un prompt que realmente funcione?

El prompt básico de arriba te lleva quizás al 75%. Tres patrones lo mejoran notablemente.

Incluye valid_actions literalmente. No resumas. No traduzcas. La lista valid_actions del servidor tiene montos exactos de min/max para raises y montos exactos de call. Si los describes en lenguaje natural, el LLM va a equivocarse en el sizing del raise unas 15% de las veces. Pasa el JSON crudo y el modelo lo usará correctamente.

Fuerza salida JSON, valida antes de enviar. Nunca confíes en que el LLM genere JSON limpio. Envuelve la llamada en try/except y haz fallback a fold si el parsing falla:

try:
    decision = json.loads(text)
    action = decision["action"]
    if action not in {"fold", "check", "call", "raise", "all_in"}:
        decision = {"action": "fold", "amount": 0}
except (json.JSONDecodeError, KeyError):
    decision = {"action": "fold", "amount": 0}

Esta es la diferencia entre un bot que funciona durante toda la temporada y otro que se bloquea en la mano 47 porque Claude añadió "Here's my decision:" antes del JSON.

Dale al modelo el historial de acciones recientes. El prompt base no tiene contexto de oponentes. Agregar las últimas 5-10 acciones de jugadores de la mano actual mejora notablemente la calidad de las decisiones. Rastrea mensajes player_action y aliméntalos como lista de "acciones recientes." No intentes alimentar el historial completo de la mano; es desperdicio y el modelo no puede usar la mayoría.

¿Cómo se ve el rendimiento de un bot LLM en el leaderboard?

Corrí un bot Claude Haiku durante una temporada completa como referencia. Estos son los números aproximados:

  • 3,200 manos jugadas en 14 días
  • Score final: 7,800 fichas (partiendo de 5,000 baseline)
  • Win rate: 24% de manos jugadas
  • bb/100: aproximadamente +1.4 (positivo pero modesto)
  • Costo total del LLM: $9.60 por la temporada

Como referencia, el líder de esa temporada terminó con unas 18.500 fichas. El bot con LLM jugó de forma sólida, aunque lejos de los mejores. Sus decisiones lo situaron en la zona media de la clasificación: evitó errores graves, pero perdió fichas contra rivales que aprovechaban sus tamaños de apuesta predecibles.

Su mayor debilidad fue el tamaño de las apuestas. El LLM tendía a apostar aproximadamente el bote completo en casi todas las situaciones. Una heurística que alternaba apuestas del 50 % y del 75 % del bote con apuestas superiores al bote, según las cartas comunitarias, obtuvo mejores resultados contra los mismos rivales.

Su mayor fortaleza fue adaptarse a situaciones nuevas. En un bote con cuatro jugadores, una mesa emparejada y dos proyectos de color, tomó decisiones razonables que los bots basados en reglas fijas suelen resolver peor. Esa capacidad resulta más útil en combinaciones de cartas comunitarias que las tablas de rangos sencillas no cubren bien.

¿Se puede combinar LLM y heurísticas?

Sí, y esta es probablemente la arquitectura más efectiva para un bot basado en LLM.

El patrón: usa heurísticas para las decisiones que puedes codificar barato (selección pre-flop, folds obvios, value bets obvios) y llama al LLM solo para los spots que requieren juicio. Esto reduce tu costo de LLM drásticamente y la calidad de las decisiones sube porque el LLM solo maneja su territorio más fuerte.

Un corte simple: sáltate la llamada LLM completamente si el spot es "trivial." Spots triviales incluyen enfrentar un raise pre-flop con 72 offsuit (siempre fold), tener la opción de check en el river con las nuts (siempre raise), o tener un cálculo de pot odds que es obviamente rentable.

def is_trivial_spot(state, hole_cards):
    # Pre-flop trash → fold
    if not state.get("community_cards"):
        if hole_cards and rank_strength(hole_cards) < 0.15:
            return ("fold", 0)
    # Free check available → take it
    actions = {a["action"]: a for a in state.get("valid_actions", [])}
    if "check" in actions and len(actions) == 1:
        return ("check", 0)
    return None  # not trivial, use LLM

Este tipo de pre-filtro cortó nuestra tasa de llamadas LLM en un 60% en las pruebas. El costo bajó de $9.60 por temporada a unos $4.20, y la calidad del juego mejoró porque el LLM solo manejaba spots donde agrega valor real.

Lo que nos equivocamos con nuestro primer bot LLM

La primera versión no tenía validación JSON, sin fallback, y sin rate limiting. En 200 manos había se bloqueado dos veces (Claude devolvió un prefijo de explicación que rompió el parser), cometió un error de sizing absurdo (Claude devolvió amount: 60000 cuando el raise máximo era 1980), y quemó tokens de API más rápido de lo esperado porque mandábamos el historial completo de la mano en cada llamada.

Los arreglos fueron aburridos pero obligatorios: validar salida JSON, limitar montos de raise a rangos válidos antes de enviar, solo mandar contexto reciente. Ninguno es emocionante, pero son la diferencia entre un bot que corre sin atención y uno que necesita niñera constante.

La otra cosa que nos equivocamos: selección de modelo. Empezamos con Sonnet porque "modelo más fuerte = mejor juego." Para decisiones de poker específicamente, Haiku es más que capaz. La calidad marginal de Sonnet no valía 5x el costo. Usa el modelo barato primero y solo actualiza si tienes evidencia de que importa.

FAQ

¿Un bot LLM le ganará a un bot heurístico afinado? Generalmente no. Un bot heurístico bien afinado con selección de manos, sizing y modelado básico de oponentes adecuados superará a un bot LLM baseline en 6-max. El bot LLM es más rápido de construir y más flexible, pero no es el enfoque más fuerte posible.

¿Cuánto cuesta una temporada de juego con LLM? Para un bot Claude Haiku jugando 3,000 manos en 14 días, espera aproximadamente $5-$10 en costos de API. Agregar un pre-filtro heurístico para saltear decisiones triviales lo reduce a $2-$5. GPT-4o-mini es comparable. Sonnet/Opus son 5-10x más caros.

¿El LLM puede ver las cartas de los oponentes? No. El mensaje your_turn solo incluye información que tu bot debería tener: el pot, community cards, tu stack, stacks de oponentes, acciones válidas. Las cartas de los oponentes se revelan solo durante el showdown vía el mensaje hand_result. El protocolo garantiza información justa.

¿Qué pasa si la llamada LLM hace timeout? Tienes 45 segundos por acción en Open Poker. Si tu llamada LLM se cuelga, tu bot hace auto-fold. Envuelve las llamadas LLM en asyncio.wait_for() con timeout de 5-10 segundos, y haz fallback a una decisión heurística (o fold) si se activa. Ve la guía de debug para más sobre timeouts de acción.

¿Puedo usar un LLM local (Llama, Mistral) en su lugar? Sí. Cualquier modelo que corra en tu hardware funciona. El tradeoff es calidad: modelos locales de 7B parámetros juegan notablemente peor que Claude o GPT-4. Modelos locales de 70B+ son competitivos pero caros de hostear. Para la mayoría de los constructores de bots, una llamada de API pagada es más barata que correr inferencia local.


Los bots con LLM son el camino más rápido para tener un motor de decisión funcional en Open Poker. No son el enfoque más fuerte posible, pero son 10x más rápidos de construir y toman decisiones razonables en la cola larga de spots inusuales. Registra un bot, consigue una API key de Claude, y tendrás un jugador LLM funcional en menos de una hora.

Seguir Leyendo