Ir al contenido
[OPEN_POKER]

Por qué tu bot de póker agota el tiempo: causas y soluciones asíncronas

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

Lección 4 de 13: Mejorar la fiabilidad

Identifica decisiones lentas antes de dejar el bot sin supervisión. Completa la lección 3, usa Python 3.11 o posterior e instala las dependencias del requirements.txt incluido en la descarga.

Un tiempo agotado de tu bot de póker retira automáticamente cualquier mano que tuvieras. Pareja de ases, color máximo, da igual: el servidor da por perdida la acción y tu pila retrocede. El plazo actual de acción en juego público es de 45 segundos. Estas son las causas de los tiempos agotados y cómo diagnosticar cada variante.

Parte de: La guía completa para crear un bot de póker con IA en 2026: el artículo principal completo sobre frameworks, lógica de decisión, equity, pruebas y dónde competir.

¿Qué ocurre cuando tu bot de póker agota el tiempo?

Open Poker aplica una ventana estricta de 45 segundos de juego público desde que tu bot recibe your_turn hasta que envía un mensaje action válido. Si no llegas a tiempo, el servidor fuerza una retirada (o un check cuando retirarse no es válido). Una desconexión no pausa ni reinicia el plazo.

La penalización por tiempo agotado es más que las fichas perdidas. El servidor marca al bot como ausente después de un tiempo agotado; tras tres manos consecutivas perdidas mientras está ausente, lo elimina de la mesa. El comportamiento completo del temporizador está documentado en la referencia de tiempos de espera de acciones.

La otra cosa que nos sorprendió es que los tiempos agotados se concentran en tus manos más fuertes. Los bots tienden a pensar más en las decisiones importantes. Una simulación Monte Carlo compleja se ejecuta más rápido con 72 offsuit (retirada instantánea) que con AKs frente a una 3-bet (cada calle requiere análisis). Las manos que más quieres jugar son aquellas en las que tu bot tiene más probabilidades de agotar el tiempo.

¿Por qué 45 segundos no son realmente generosos?

Hay tres razones, en orden de frecuencia entre los bots que vemos en producción.

1. Llamadas de red síncronas dentro del bucle de decisión. Esta es la causa número uno que hemos depurado. Un bot llama a un evaluador de manos que consulta una API externa, pregunta a una base de datos por las estadísticas de los oponentes o usa requests.get() en lugar de un cliente asíncrono. Cada llamada síncrona bloquea el bot entero. Si usas websockets y asyncio (la pila estándar), una sola llamada síncrona congela el bucle de eventos hasta que termina. Cuando el bucle está congelado, no solo tardas en esta decisión: acumulas mensajes futuros que se procesan tarde, después de que termina la decisión actual.

2. Reconexiones durante una decisión. La conexión de Open Poker es persistente, pero puede interrumpirse. Si tu red falla brevemente mientras el bot está decidiendo, el WebSocket se desconecta, el envío de la acción falla silenciosamente y esperas a que se agote el tiempo. Los bots sin lógica de reconexión simplemente se quedan colgados. Los bots con una lógica de reconexión defectuosa intentan reconectar de forma síncrona y duplican la latencia.

3. Pausas del recolector de basura en bots de larga duración. Es menos frecuente, pero sucede. Un bot que lleva 6 horas funcionando ha acumulado decenas de miles de estados de partida en memoria. El recolector de basura de Python pausa ocasionalmente la ejecución durante cientos de milisegundos para limpiar. La mayoría de las veces es invisible. A veces ocurre durante una decisión y pierdes el temporizador.

¿Cómo encuentras qué decisiones son lentas?

Añade registros de latencia a tu manejador de acciones. No esperes a tener un problema; instrumenta el bot desde el primer día. El patrón es muy sencillo:

import time
 
async def handle_your_turn(msg, ws):
    start = time.monotonic()
    try:
        action = await decide(msg)
        await ws.send(json.dumps({
            "type": "action",
            "hand_id": msg["hand_id"],
            "action": action["type"],
            **({"amount": action["amount"]} if "amount" in action else {}),
            "client_action_id": f"a-{msg['turn_token'][:8]}",
            "turn_token": msg["turn_token"],
        }))
    finally:
        elapsed_ms = (time.monotonic() - start) * 1000
        if elapsed_ms > 1000:
            print(f"[SLOW] decision took {elapsed_ms:.0f}ms on hand {msg.get('hand_number')}")
        if elapsed_ms > 5000:
            print(f"[VERY SLOW] {elapsed_ms:.0f}ms, investigate")

El umbral de 1000 ms es arbitrario, pero útil. La mayoría de las decisiones del bot debería tardar menos de 200 ms. Todo lo que supere un segundo merece una alerta porque anticipa tiempos agotados cuando haya carga. El umbral de 5000 ms detecta los casos catastróficos.

Ejecuta esto durante una hora, ordena las decisiones lentas por tiempo transcurrido y observa qué tienen en común. Verás un patrón en los primeros 50 eventos lentos: la misma cantidad de oponentes, la misma textura de la mesa y la misma rama de decisión. Ahí está el origen de la lentitud.

¿Cómo corriges las llamadas de red síncronas?

Convierte todo a asíncrono. La pila estándar de Python para un bot de Open Poker es websockets (asíncrono), asyncio (asíncrono) e idealmente httpx en lugar de requests para cualquier HTTP externo. Si llamas a un LLM, usa el cliente asíncrono (AsyncAnthropic en lugar de Anthropic, AsyncOpenAI en lugar de OpenAI).

La forma incorrecta:

import requests
 
async def decide(msg):
    # This blocks the entire event loop
    response = requests.get("https://api.example.com/equity")
    equity = response.json()["equity"]
    return ("call" if equity > 0.4 else "fold")

La forma correcta:

import httpx
 
http = httpx.AsyncClient(timeout=3.0)
 
async def decide(msg):
    response = await http.get("https://api.example.com/equity")
    equity = response.json()["equity"]
    return ("call" if equity > 0.4 else "fold")

Observa el tiempo de espera explícito del cliente HTTP. Es fundamental. Una API externa que se queda colgada para siempre colgará también tu bot. Un tiempo de espera de 3 segundos permite que tu decisión se degrade con elegancia (recurriendo a una heurística) en lugar de hacerlo de forma catastrófica.

Para las consultas a bases de datos, usa un controlador asíncrono. asyncpg para Postgres, aiomysql para MySQL y motor para MongoDB. Si usas SQLAlchemy, cambia a su API asíncrona. Los controladores síncronos dentro de bucles de eventos asíncronos son la fuente más común de tiempos agotados que vemos.

¿Cómo envuelves las decisiones con un tiempo límite?

Aunque todo tu código asíncrono sea correcto, debes limitar de forma preventiva cuánto puede tardar una decisión. Usa asyncio.wait_for() para acotar la duración de cualquier decisión asíncrona individual:

import asyncio
 
async def decide_with_fallback(msg):
    try:
        return await asyncio.wait_for(decide(msg), timeout=10.0)
    except asyncio.TimeoutError:
        print(f"[TIMEOUT] decision exceeded 10s, falling back")
        return fallback_decision(msg)
 
def fallback_decision(msg):
    """Safe default if main decision logic times out."""
    actions = {a["action"]: a for a in msg["valid_actions"]}
    if "check" in actions:
        return ("check", 0)
    if "call" in actions:
        call_amt = actions["call"]["amount"]
        # Only call if it's small
        if call_amt < msg.get("pot", 0) * 0.2:
            return ("call", call_amt)
    return ("fold", 0)

wait_for() cancela la corrutina que está esperando; no detiene por la fuerza el trabajo limitado por CPU que ya se esté ejecutando en un hilo o proceso. Mantén acotado el trabajo de CPU o aíslalo en un trabajador que puedas terminar, y haz que la ruta alternativa sea siempre rápida.

Diez segundos dejan un margen útil frente al tiempo de espera de 45 segundos del servidor. Si tu lógica principal supera los 10 segundos, algo va mal, y recurrir a un valor predeterminado seguro es mejor que apostar a que la ruta lenta acabará a tiempo.

La decisión alternativa debe ser conservadora. Una retirada predeterminada segura está bien. Un check predeterminado seguro es mejor. No intentes hacer «inteligente» la alternativa; su objetivo es ser rápida y predecible.

¿Cómo gestionas las reconexiones sin perder la ventana de acción?

La lógica de reconexión es donde vemos los errores de tiempo agotado más sutiles. El patrón incorrecto:

async for raw in ws:
    msg = json.loads(raw)
    if msg["type"] == "your_turn":
        if not ws.open:
            # Try to reconnect synchronously...
            ws = await reconnect()  # blocks for seconds
        await handle_your_turn(msg, ws)

El problema es que cuando se evalúa if not ws.open, ya estás dentro del manejador del mensaje. El iterador asíncrono está pausado. Si reconectar tarda 5 segundos, ya has consumido 5 segundos de tu ventana de acción antes siquiera de empezar a decidir. Peor aún, el your_turn original se envió a la conexión antigua, que ahora está muerta. El servidor sigue esperando una respuesta en una conexión que ya no existe.

Recupérate fuera del bucle de mensajes y solicita el estado actual antes de decidir. Una desconexión no invalida por sí sola un turno ni reinicia su plazo. La instantánea de resincronización del jugador activo puede restaurar el token existente. No repitas ciegamente decisiones antiguas ni envíes join_lobby mientras ya estás sentado.

La descarga del curso ejecutable conserva el ID de la mesa durante una reconexión activa. Después de connected, envía resync_request; solo actúa si snapshot.hero contiene tanto valid_actions como turn_token, usando el hand_id de la instantánea. Un proceso nuevo necesita descubrir la partida activa y hacer un seguimiento persistente de los reconocimientos antes de estar listo para funcionar sin supervisión. La guía de recuperación enlazada describe esos requisitos adicionales.

¿Qué ocurre con las pausas del recolector de basura?

En los bots de larga duración, las pausas del recolector son reales, pero fáciles de mitigar. Hay dos soluciones prácticas.

Limita la retención del historial de manos. Un bot que guarda cada mano en una lista crece linealmente para siempre. Después de 10.000 manos, esa lista contiene 10.000 diccionarios con listas de cartas, listas de acciones e historiales de botes incrustados. El recolector tiene más trabajo que hacer en cada ciclo. Limita el historial a las últimas 500 manos:

from collections import deque
recent_hands = deque(maxlen=500)

deque con maxlen usa memoria constante independientemente del número de manos que introduzcas. Las manos antiguas desaparecen automáticamente por el extremo.

Ejecuta gc.collect() entre manos cuando importe la latencia. Activar manualmente la recolección en una ventana tranquila conocida (entre manos, no durante una decisión) te permite controlar cuándo ocurren las pausas. Añade una llamada a gc.collect() dentro de tu manejador hand_result, no dentro del manejador your_turn.

¿Cómo se compara esto con las plataformas comerciales de bots?

Las plataformas comerciales de bots, como Pluribus (Meta, 2019), funcionaban en infraestructura dedicada con redes personalizadas, planificación en tiempo real y presupuestos de decisión de varios segundos. El artículo publicado sobre Pluribus señaló tiempos medios de decisión de 20 segundos para Pluribus y aún mayores para los competidores basados en solvers.

La ventana de 45 segundos de juego público de Open Poker deja margen para el cálculo normal, pero no garantiza que terminen las llamadas lentas a un LLM o las ejecuciones Monte Carlo. Si estás llegando al límite, instrumenta la ruta de decisión y reduce o aísla el trabajo lento. La documentación del ciclo de vida del bot cubre la máquina de estados de conexión.

Preguntas frecuentes

¿Cuál es el tiempo de espera de una acción en Open Poker? El tiempo de espera de una acción en juego público es de 45 segundos desde your_turn hasta tu respuesta action. Si tu bot no responde a tiempo, el servidor se retira automáticamente (o hace check automáticamente cuando retirarse no es válido). Los tiempos agotados repetidos pueden eliminar tu bot de la mesa.

¿Por qué mi bot solo agota el tiempo en las decisiones difíciles? Porque las decisiones difíciles activan más rutas de código. Las llamadas de red síncronas, los calculadores de equity y las consultas de perfiles de oponentes se ejecutan más a menudo cuando tu bot tiene más que analizar. La solución es hacer asíncronas esas rutas de código y añadir un tiempo límite alternativo a tu función de decisión principal.

¿Puedo ampliar el tiempo de espera de acciones de mi bot? No. El temporizador de 45 segundos de juego público es fijo para todos los bots. Una desconexión no lo pausa ni lo reinicia, así que mantén preparada una alternativa y utiliza el estado de acción más reciente después de reconectar.

¿Agotar el tiempo me cuesta fichas? No directamente: el servidor se retira automáticamente, lo que te cuesta lo que ya estuviera en el bote. No hay una penalización adicional. Sin embargo, agotar el tiempo repetidamente te desconecta de la mesa, lo que te cuesta tiempo de juego y posibles ganancias en manos futuras.

¿Cuál es una latencia razonable para las decisiones de mi bot? Menos de 200 ms es excelente. Menos de 1 segundo es normal. Más de 5 segundos significa que tienes una llamada síncrona en alguna parte de la pila. Más de 30 segundos significa que debes refactorizar antes de volver a desplegar.


Los tiempos agotados son el error invisible que destruye los porcentajes de victorias de los bots. La solución es principalmente preventiva: registra la latencia de cada decisión, envuelve la lógica principal en asyncio.wait_for(), usa clientes asíncronos cuando corresponda y conserva una alternativa segura. Construye tu primer bot con estos patrones desde el principio.

Comprueba el resultado de la lección 4

Qué cambia
Mide el tiempo de decisión y usa una acción alternativa si se supera el límite local.
Salida esperada
max_decision_ms: un valor medido
Comprueba que funciona
Comprueba el tiempo medido. El límite local no reinicia el plazo del servidor ni interrumpe una tarea bloqueada.

Descomprime el archivo de la lección, abre la carpeta en una terminal y ejecuta:

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

La prueba usa datos simulados sin conexión y muestra checkpoint: passed. Para jugar en línea necesitas OPEN_POKER_API_KEY en el entorno de ejecución y rivales disponibles. Consulta el README incluido para configurar el bot y conocer los límites de recuperación.

Todas las lecciones del curso
  1. 1. Crea un bot de póker en Python
  2. 2. Arquitectura básica de un bot de póker
  3. 3. Cómo depurar errores de WebSocket en tu bot
  4. 4. Por qué tu bot se queda sin tiempo
  5. 5. Matemáticas del póker para bots
  6. 6. Rangos por posición para bots de póker
  7. 7. Estrategia de apuestas para bots de póker
  8. 8. Tutorial de PokerKit
  9. 9. Calculadora de equity por Monte Carlo
  10. 10. Modelado de rivales
  11. 11. Arquitectura avanzada de un bot de póker
  12. 12. Cómo funciona la puntuación de la clasificación
  13. 13. Arquitectura profesional de un bot de póker

Seguir Leyendo