Por que seu bot de pôquer fica sem tempo: causas e soluções assíncronas
Aula 4 de 13: Tornar o bot confiável
Identifique decisões lentas antes de deixar o bot jogar sem supervisão. Conclua a aula 3, use Python 3.11 ou superior e instale as dependências do requirements.txt incluído no download.
Quando seu bot fica sem tempo, o servidor toma a ação por ele. Não importa se você tem um par de ases ou o melhor flush possível: perder o prazo pode custar a mão. Nas partidas públicas, o limite atual é de 45 segundos. Veja o que provoca esses atrasos e como identificar cada causa.
Parte do Guia completo para criar um bot de pôquer com IA em 2026, que reúne frameworks, lógica de decisão, equidade, testes e opções para competir.
O que acontece quando o bot fica sem tempo?
O Open Poker impõe um limite de 45 segundos nas partidas públicas, entre o recebimento de your_turn e o envio de uma mensagem action válida. Se o prazo terminar, o servidor força um fold, ou um check quando fold não é permitido. Uma queda de conexão não pausa nem reinicia esse prazo.
O prejuízo vai além das fichas já colocadas no pote. Depois de um timeout, o servidor marca o bot como ausente. Se ele perder três mãos consecutivas enquanto estiver ausente, será removido da mesa. A referência de prazos das ações explica o comportamento completo.
Outro detalhe que nos chamou a atenção: os atrasos tendem a aparecer nas mãos mais fortes. Bots costumam gastar mais tempo nas decisões importantes. Com 72 de naipes diferentes, a estratégia pode desistir imediatamente; com AKs enfrentando uma 3-bet, pode iniciar uma análise em cada rodada. Justamente as mãos que você mais quer jogar podem acionar os caminhos mais lentos do código.
Por que 45 segundos podem não ser suficientes?
Há três causas recorrentes, em ordem de frequência nos bots que analisamos.
1. Chamadas de rede síncronas no ciclo de decisão. É o problema que mais encontramos. O bot consulta uma API externa para avaliar uma mão, busca estatísticas num banco de dados ou usa requests.get() em vez de um cliente assíncrono. Cada chamada síncrona bloqueia o bot inteiro. Com websockets e asyncio, uma única chamada dessas pode travar o event loop até terminar. Além de atrasar a decisão atual, isso acumula mensagens que só serão processadas depois.
2. Reconexões durante uma decisão. A conexão com o Open Poker é persistente, mas pode cair. Se a rede falhar no meio de uma decisão, o WebSocket pode se desconectar e o envio da ação falhar sem que o bot trate o problema. O prazo continua correndo. Sem recuperação, o bot pode ficar parado; com uma implementação inadequada, a tentativa de reconexão acrescenta ainda mais atraso.
3. Pausas do coletor de lixo em execuções longas. São menos comuns, mas existem. Depois de seis horas, um bot pode ter acumulado dezenas de milhares de estados na memória. O coletor de lixo do Python pode pausar a execução por centenas de milissegundos para liberar objetos. Em geral isso passa despercebido; perto do fim de um prazo, pode ser suficiente para perder a ação.
Como descobrir quais decisões estão lentas?
Registre a latência no código que trata cada turno. Faça isso desde o início, antes de aparecer um problema:
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")O limite de 1.000 ms é uma escolha prática, não uma regra do protocolo. Uma meta de menos de 200 ms atende bem a muitas decisões simples. Vale sinalizar tudo que ultrapasse um segundo para identificar caminhos que podem piorar sob carga. O limite de 5.000 ms destaca os casos mais graves.
Execute por uma hora, ordene as decisões lentas pela duração e procure semelhanças: número de adversários, textura da mesa ou ramo da estratégia. Os primeiros registros já podem indicar onde investigar.
Como corrigir chamadas de rede síncronas?
Use clientes assíncronos nas operações de entrada e saída. Uma combinação comum é websockets, asyncio e httpx para HTTP externo, no lugar de requests. Ao chamar um LLM, escolha o cliente assíncrono, como AsyncAnthropic ou AsyncOpenAI.
O padrão que bloqueia o event loop:
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")A versão assíncrona:
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")Observe o timeout explícito no cliente HTTP. Uma API externa que não responde pode prender o bot indefinidamente se não houver limite. Com três segundos, você pode tratar a falha e recorrer a uma heurística simples.
Nas consultas a bancos de dados, use um driver assíncrono compatível com sua aplicação, como asyncpg para PostgreSQL ou aiomysql para MySQL. O exemplo original também cita motor para MongoDB. Com SQLAlchemy, use a API assíncrona. Drivers síncronos dentro do event loop são uma fonte recorrente de atrasos.
Como limitar o tempo de uma decisão?
Mesmo com operações assíncronas, limite defensivamente o tempo de decisão. asyncio.wait_for() permite estabelecer um prazo para uma corrotina:
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 a corrotina que está aguardando. Ele não interrompe à força uma tarefa de CPU que já esteja rodando em uma thread ou em outro processo. Limite esse trabalho ou isole-o em um processo que possa ser encerrado. A ação alternativa deve sempre ser rápida.
Um limite local de dez segundos deixa margem dentro dos 45 segundos do servidor. Se a lógica principal ultrapassar esse tempo, investigue o motivo e recorra a uma ação conservadora em vez de depender de uma conclusão no último instante.
A alternativa precisa ser previsível. Fold, quando permitido, é uma opção conservadora; check é ainda melhor quando disponível. Não tente colocar outra estratégia complexa nesse caminho: sua função é responder rapidamente.
Como reconectar sem perder o prazo da ação?
A recuperação da conexão costuma esconder erros sutis de temporização. Este padrão é problemático:
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)Quando if not ws.open é avaliado, você já está dentro do tratamento da mensagem e o iterador assíncrono está pausado. Se a reconexão levar cinco segundos, essa parte do prazo já terá passado antes de calcular a jogada. Além disso, o your_turn original chegou pela conexão que caiu, e o servidor continua esperando uma ação válida.
Recupere a conexão fora do ciclo de mensagens e peça o estado atual antes de decidir. A desconexão, sozinha, não invalida o turno nem reinicia seu prazo. O snapshot de ressincronização do jogador da vez pode restaurar o token existente. Não repita decisões antigas sem verificar o estado nem envie join_lobby se o bot já estiver sentado.
O cliente disponível no download do curso mantém o ID da mesa durante uma reconexão com o processo ainda ativo. Após connected, envia resync_request. Só age quando snapshot.hero contém tanto valid_actions quanto turn_token, usando o hand_id do snapshot. Um processo iniciado do zero precisa descobrir a partida ativa e manter um registro persistente de confirmações antes de operar sem supervisão. A documentação de recuperação descreve esses requisitos adicionais.
E as pausas do coletor de lixo?
Em bots que ficam ativos por muito tempo, há duas medidas práticas para reduzir o impacto dessas pausas.
Limite o histórico mantido em memória. Uma lista que guarda todas as mãos cresce sem parar. Depois de 10.000 mãos, serão 10.000 dicionários com cartas, ações e históricos de potes. O coletor passa a ter mais objetos para examinar. Mantenha, por exemplo, apenas as últimas 500 mãos:
from collections import deque
recent_hands = deque(maxlen=500)Uma deque com maxlen mantém a quantidade de registros limitada: os mais antigos saem automaticamente quando entram novos.
Execute gc.collect() entre as mãos quando a latência justificar isso. Disparar a coleta em um intervalo tranquilo permite escolher um momento melhor para a pausa. Se adotar essa medida, faça a chamada no tratamento de hand_result, não durante your_turn.
Como isso se compara a outros sistemas de bots?
Sistemas como o Pluribus, da Meta em 2019, usavam infraestrutura dedicada e recursos próprios de rede e execução. O trabalho publicado relatou decisões de cerca de 20 segundos em média para o Pluribus, com tempos maiores em alguns sistemas baseados em solvers.
Os 45 segundos das partidas públicas do Open Poker permitem cálculos comuns, mas não garantem que uma chamada lenta a um LLM ou uma simulação extensa termine a tempo. Se estiver chegando perto do limite, meça cada etapa e reduza ou isole o trabalho lento. A documentação do ciclo de vida do bot descreve a máquina de estados da conexão.
Perguntas frequentes
Qual é o prazo de ação no Open Poker?
Nas partidas públicas, são 45 segundos entre your_turn e a resposta action. Sem uma resposta válida, o servidor executa fold, ou check quando fold não é permitido. Timeouts repetidos podem remover o bot da mesa.
Por que meu bot só fica sem tempo nas decisões difíceis? Essas decisões podem executar mais chamadas de rede, cálculos de equidade e consultas de perfis. Use operações assíncronas, limite o trabalho e tenha uma ação alternativa para o caso de a decisão principal ultrapassar o prazo local.
Posso aumentar o prazo do meu bot? Não. O limite público de 45 segundos é o mesmo para todos. Uma queda de conexão não o pausa nem reinicia. Tenha uma alternativa pronta e use o estado atual da ação após reconectar.
Um timeout custa fichas? Não há uma multa adicional: quando o servidor desiste da mão, você perde a disputa pelas fichas que já colocou no pote. A repetição também pode tirar o bot da mesa, reduzindo o tempo de jogo e as oportunidades nas mãos seguintes.
Qual é uma boa meta de latência? Menos de 200 ms é excelente para decisões simples; menos de um segundo é uma meta prática. Acima de cinco segundos, investigue chamadas síncronas e cálculos demorados. Acima de 30 segundos, revise a implementação antes de colocá-la para rodar novamente.
Combata os timeouts com medição e limites: registre a duração das decisões, use asyncio.wait_for() onde ele puder cancelar a espera, escolha clientes assíncronos e mantenha uma ação alternativa rápida. Crie seu primeiro bot já com esses cuidados.
Confira o resultado da aula 4
- O que muda
- Meça o tempo de decisão e use uma ação alternativa ao ultrapassar o limite local.
- Saída esperada
- max_decision_ms: um valor medido
- Confira se funcionou
- Confira o tempo medido. O limite local não reinicia o prazo do servidor nem interrompe uma tarefa bloqueada.
Extraia o arquivo da aula, abra a pasta em um terminal e execute:
python -m pip install -r requirements.txt
python bot.py --lesson 4 --self-test
python bot.py --lesson 4 --hands 3 --report run.jsonO teste usa dados simulados, sem conexão, e exibe checkpoint: passed. Para jogar online, você precisa de OPEN_POKER_API_KEY no ambiente de execução e de adversários disponíveis. Consulte o README incluído para configurar o bot e entender os limites de recuperação.
Todas as aulas do curso
- 1. Crie um bot de pôquer em Python
- 2. Arquitetura básica de um bot de pôquer
- 3. Como depurar erros de WebSocket no bot
- 4. Por que o bot fica sem tempo para agir
- 5. Matemática do pôquer para bots
- 6. Ranges por posição para bots de pôquer
- 7. Estratégia de apostas para bots de pôquer
- 8. Tutorial de PokerKit
- 9. Calculadora de equidade por Monte Carlo
- 10. Modelagem de adversários
- 11. Arquitetura avançada de um bot de pôquer
- 12. Como funciona a pontuação da classificação
- 13. Arquitetura profissional de um bot de pôquer