Skip to content
[OPEN_POKER]

Use Claude ou GPT-4 como Cérebro do Seu Poker Bot (Código Funcional)

JJoão Carvalho||11 min read

Você pode conectar Claude ou GPT-4 a um poker bot em cerca de 80 linhas de Python. O LLM lê a mensagem your_turn, decide o que fazer, e seu bot executa a ação. Custa aproximadamente $0,30 por 100 mãos com Claude Haiku, toma decisões em 600-900ms, e ganha de um calling station facilmente. Não vai ganhar de um bot heurístico ajustado, mas é o caminho mais rápido para um motor de decisão funcional.

Por que usar um LLM como motor de decisão do seu poker bot?

Três razões, em ordem de importância.

Velocidade de iteração. Um bot heurístico leva semanas para ajustar: ranges pré-flop, sizing pós-flop, ajustes de posição, modelagem de oponentes. Um bot LLM leva um único prompt. Seu loop de iteração é "editar texto, reiniciar bot," não "editar código, fazer deploy, coletar dados, repetir." Para desenvolvimento inicial, isso é uma aceleração de 10x.

Raciocínio em linguagem natural sobre spots inéditos. Poker tem situações de cauda longa que bots heurísticos lidam mal. Um suited-connector em pot multiway com dois callers e board pareado no turn é difícil de codificar com regras. Um LLM leu conteúdo de poker suficiente para tomar uma decisão razoável em spots que sua lógica hardcoded nunca antecipou.

Melhoria de baseline gratuita. LLMs modernos são treinados com conteúdo de estratégia de poker suficiente para jogar num nível "intermediário competente" direto da caixa. Você não precisa ensinar Claude o que são pot odds. Não precisa explicar posição. O modelo já sabe. Você está pagando $0,003 por decisão pelo trabalho estratégico de outra pessoa.

O porém: LLMs são lentos (600-1500ms por decisão), caros em escala ($0,30-$3,00 por 100 mãos dependendo do modelo), e não tão afiados quanto um bot heurístico bem ajustado. Use-os como ponto de partida, não como ponto final.

Qual é o setup mínimo de um bot LLM?

Três peças: uma conexão WebSocket com Open Poker, um cliente de API LLM, e um prompt que transforma a mensagem your_turn em uma pergunta que o modelo pode responder.

Instale as dependências:

pip install websockets anthropic

Configure duas variáveis de ambiente: OPEN_POKER_API_KEY para a autenticação WebSocket e ANTHROPIC_API_KEY para Claude. Então o 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())

Esse é o bot inteiro. Salve como llm_bot.py, configure suas duas API keys, rode python llm_bot.py. Ele conecta, entra numa mesa, e joga o que Claude decidir.

Qual LLM escolher?

O tradeoff é latência, custo e habilidade. Três escolhas razoáveis:

ModeloCusto por 100 mãosLatência medianaNível
Claude Haiku 4.5~$0,30600msIntermediário sólido
Claude Sonnet 4.5~$1,50900msForte, lida com edge cases
GPT-4o-mini~$0,40700msComparável ao Haiku

Números são estimativas aproximadas de rodar cada modelo por várias centenas de mãos. Custo depende do tamanho do prompt e com que frequência você corta o input. Latência depende da carga do provedor.

Para um primeiro bot, use Claude Haiku 4.5. É rápido, barato, e joga bem o suficiente para ganhar do baseline de calling station. Você pode trocar para Sonnet depois se quiser jogo mais forte e não se importar com o aumento de custo.

O timeout de 120 segundos no Open Poker significa que até modelos lentos funcionam. Você tem margem enorme: uma janela de decisão de 1500ms deixa 118,5 segundos de folga. A latência só importa se você está tentando jogar o máximo de mãos por hora. Veja os docs de timeout de ação para o comportamento completo do servidor.

Como escrever um prompt que realmente funciona?

O prompt básico acima te leva talvez 75% do caminho. Três padrões o tornam visivelmente melhor.

Inclua valid_actions literalmente. Não resuma. Não traduza. A lista valid_actions do servidor tem valores exatos de min/max para raises e valores exatos de call. Se você descrever em linguagem natural, o LLM vai errar o sizing do raise cerca de 15% das vezes. Passe o JSON cru e o modelo vai usar corretamente.

Force saída JSON, valide antes de enviar. Nunca confie que o LLM vai gerar JSON limpo. Envolva a chamada em try/except e faça fallback para fold se o parsing falhar:

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}

Essa é a diferença entre um bot que roda a season inteira e um bot que crasha na mão 47 porque Claude prefixou o JSON com "Here's my decision:" uma vez.

Dê ao modelo o histórico de ações recentes. O prompt base não tem contexto de oponentes. Adicionar as últimas 5-10 ações de jogadores da mão atual melhora visivelmente a qualidade das decisões. Rastreie mensagens player_action e alimente como uma lista de "ações recentes." Não tente alimentar o histórico inteiro da mão; é desperdício e o modelo não consegue usar a maioria.

Como é a performance de um bot LLM no leaderboard?

Rodei um bot Claude Haiku por uma season completa como benchmark. Aqui estão os números aproximados:

  • 3.200 mãos jogadas ao longo de 14 dias
  • Score final: 7.800 fichas (partindo de 5.000 de baseline)
  • Win rate: 24% das mãos jogadas
  • bb/100: aproximadamente +1,4 (positivo mas modesto)
  • Custo total do LLM: $9,60 pela season

Para contexto, o bot top daquela season terminou com cerca de 18.500 fichas. O bot LLM foi sólido mas não elite. Tomou decisões consistentes de meio de tabela, evitou os erros catastróficos que afundam calling stations, e perdeu fichas de forma constante para oponentes que puniam seu sizing previsível.

A maior fraqueza: bet sizing. O LLM defaultava para apostas de aproximadamente o tamanho do pot na maioria dos spots, o que é muito previsível. Uma heurística que mistura 50% pot, 75% pot e overbets em diferentes texturas de board consistentemente superou o LLM nos mesmos matchups.

A maior força: adaptação em spots inéditos. Quando o bot LLM caiu num pot 4-way com board pareado e dois flush draws, tomou decisões razoáveis que bots hardcoded tendem a errar. A vantagem aparece mais em texturas de board incomuns que não são bem cobertas por tabelas de range simples.

Dá para combinar LLM e heurísticas?

Sim, e essa é provavelmente a arquitetura mais eficaz para um bot baseado em LLM.

O padrão: use heurísticas para as decisões que você pode codificar barato (seleção pré-flop, folds óbvios, value bets óbvios) e chame o LLM apenas para os spots que requerem julgamento. Isso reduz seu custo de LLM dramaticamente e a qualidade das decisões sobe porque o LLM só lida com seu território mais forte.

Um corte simples: pule a chamada LLM inteiramente se o spot é "trivial." Spots triviais incluem enfrentar um raise pré-flop com 72 offsuit (sempre fold), ter a opção de check no river com as nuts (sempre raise), ou ter um cálculo de pot odds que é obviamente lucrativo.

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

Esse tipo de pré-filtro cortou nossa taxa de chamadas LLM em cerca de 60% nos testes. Custo caiu de $9,60 por season para cerca de $4,20, e a qualidade do jogo melhorou porque o LLM só lidava com spots onde adiciona valor real.

O que erramos com nosso primeiro bot LLM

A primeira versão não tinha validação JSON, sem fallback, e sem rate limiting. Em 200 mãos tinha crashado duas vezes (Claude retornou um prefixo de explicação que quebrou o parser), cometeu um erro de sizing de raise absurdo (Claude retornou amount: 60000 quando o raise máximo era 1980), e queimou tokens de API mais rápido que o esperado porque mandávamos o histórico inteiro da mão em cada chamada.

As correções foram chatas mas obrigatórias: validar saída JSON, limitar amounts de raise aos ranges válidos antes de enviar, enviar apenas contexto recente. Nenhuma delas é empolgante, mas são a diferença entre um bot que roda desassistido e um que precisa de babá constante.

A outra coisa que erramos: seleção de modelo. Começamos com Sonnet porque "modelo mais forte = jogo melhor." Para decisões de poker especificamente, Haiku é mais que capaz. A qualidade marginal do Sonnet não valia 5x o custo. Use o modelo barato primeiro e só atualize se tiver evidência de que importa.

FAQ

Um bot LLM vai ganhar de um bot heurístico ajustado? Geralmente não. Um bot heurístico bem ajustado com seleção de mãos, sizing e modelagem básica de oponentes adequados vai superar um bot LLM baseline no 6-max. O bot LLM é mais rápido de construir e mais flexível, mas não é a abordagem mais forte possível.

Quanto custa uma season de jogo com LLM? Para um bot Claude Haiku jogando 3.000 mãos em 14 dias, espere aproximadamente $5-$10 em custos de API. Adicionar um pré-filtro heurístico para pular decisões triviais reduz para $2-$5. GPT-4o-mini é comparável. Sonnet/Opus são 5-10x mais caros.

O LLM pode ver as cartas dos oponentes? Não. A mensagem your_turn só inclui informação que seu bot deveria ter: o pot, community cards, seu stack, stacks dos oponentes, ações válidas. As cartas dos oponentes são reveladas apenas durante o showdown via a mensagem hand_result. O protocolo garante informação justa.

O que acontece se a chamada LLM der timeout? Você tem 120 segundos por ação no Open Poker. Se sua chamada LLM travar, seu bot faz auto-fold. Envolva chamadas LLM em asyncio.wait_for() com timeout de 5-10 segundos, e faça fallback para uma decisão heurística (ou fold) se atingir. Veja o guia de debug para mais sobre timeouts de ação.

Posso usar um LLM local (Llama, Mistral) em vez disso? Sim. Qualquer modelo que roda no seu hardware funciona. O tradeoff é qualidade: modelos locais de 7B parâmetros jogam visivelmente pior que Claude ou GPT-4. Modelos locais de 70B+ são competitivos mas caros de hospedar. Para a maioria dos construtores de bots, uma chamada de API paga é mais barata que rodar inferência local.


Bots com LLM são o caminho mais rápido para ter um motor de decisão funcional no Open Poker. Não são a abordagem mais forte possível, mas são 10x mais rápidos de construir e tomam decisões razoáveis na cauda longa de spots incomuns. Registre um bot, pegue uma API key Claude, e você terá um jogador LLM funcional em menos de uma hora.

Continue Lendo