Skip to content
[OPEN_POKER]

Arquitetura Básica de Poker Bot: Um Núcleo Confiável

JJoão Carvalho||11 min read

Um poker bot básico precisa de três coisas antes de precisar de inteligência: um loop WebSocket correto, um objeto pequeno para o estado da mão e uma proteção capaz de enviar apenas ações legais. Construa isso primeiro. Um bot que joga com regras modestas por 1.000 mãos é uma base melhor do que uma policy inteligente que perde o token do turno na décima segunda mão.

Série sobre arquitetura de poker bots: Parte 1 de 3. Continue com Arquitetura Avançada de Poker Bots e depois leia Arquitetura Profissional de Poker Bots. Se você também está planejando o orçamento, veja Custo de um Poker Bot em 2026.

O que faz parte da arquitetura básica de um poker bot?

A arquitetura básica de um poker bot tem quatro partes pequenas: transporte, estado, policy e validação de ações. O transporte lê e escreve JSON. O estado guarda a mão atual e as cartas fechadas. A policy propõe uma ação. A validação compara essa proposta com valid_actions antes que qualquer coisa atravesse o socket.

Mantenha essas partes separadas, mesmo que o primeiro bot caiba em um único arquivo. Essa separação não é mera formalidade. Ela dá um endereço a cada bug. Se o servidor rejeitar um raise, inspecione a validação. Se o bot achar que ainda tem as cartas da mão anterior, inspecione o estado. Se ele der calls demais, inspecione a policy. Aprendemos isso depois de criar bots iniciais cujo loop do socket, regras de cartas e logs compartilhavam o mesmo dicionário mutável. Uma reconexão podia parecer uma falha de estratégia.

ParteEntradaSaídaInvariante básico
TransporteFrames WebSocketMensagens processadasNunca inventa o estado do jogo
EstadoEventos do servidorSnapshot da mão atualReinicia em hand_start
PolicySnapshot e ações legaisIntenção de açãoNunca escreve no socket
ProteçãoIntenção e valid_actionsAção do protocoloEnvia apenas uma ação oferecida

Esse design é propositalmente menos ambicioso do que nosso guia de arquitetura de software para poker bots. No nível básico, quatro limites bastam.

Quais mensagens do Open Poker a primeira versão precisa tratar?

A primeira versão precisa reagir a hole_cards, your_turn, action_rejected, table_closed e season_ended. Ela também deve registrar connected, table_joined, hand_start, player_action, community_cards e hand_result. Apenas your_turn exige uma ação de poker, mas os outros eventos mantêm corretos o estado e o comportamento da sessão.

O protocolo de ações atual do Open Poker exige três campos de correlação em cada ação: hand_id, o turn_token mais recente e um novo client_action_id. A ausência dos campos V2 é rejeitada como legacy_action_protocol. Um ID de mão antigo resulta em stale_hand_action, e um token antigo resulta em stale_turn_token. Trate esses campos como um envelope indivisível em torno da resposta da policy.

O servidor também informa o que é legal. Uma entrada raise contém limites min e max exatos, e seu valor representa o total da aposta após o raise. Se a aposta atual é 20 e você quer chegar a um total de 60, envie 60, não 40. Um call não precisa de valor porque o servidor já conhece o preço. A referência de ações é a fonte oficial, enquanto o guia de tratamento de mensagens mostra o formato de cada evento.

Como criar um núcleo Python executável?

Comece com uma dependência e um arquivo. Python 3.10 ou mais recente é uma base confortável, e o cliente atual de websockets aceita cabeçalhos da requisição por meio de additional_headers.

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

Salve o código a seguir como basic_bot.py. Defina OPENPOKER_API_KEY no ambiente antes de executá-lo. A policy é propositalmente restrita e simples: ela dá check de graça, joga pares e cartas broadway, paga apenas quando o preço é de no máximo 10% do pot exibido e dá fold nos outros casos.

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

Execute com python basic_bot.py. Esse código é uma base de aprendizado, não uma estratégia vencedora. Sua propriedade útil é que toda ação passa por uma única proteção e carrega os campos exatos do turno atual.

O servidor deve ser o dono do estado legal porque uma reconstrução feita pelo cliente pode ficar incompleta após reconexões, ações rejeitadas, pots divididos ou eventos perdidos. Use pot, valid_actions, hand_id e turn_token da mensagem your_turn mais recente. Seu estado local adiciona contexto estratégico, mas não se sobrepõe ao contrato da conexão.

Essa regra evita um erro comum em primeiros bots. Um desenvolvedor soma cada player_action.amount para estimar o pot, mas amount pode ser null em checks e folds, e a semântica do valor difere entre calls e o total após o raise. Então uma reconexão pula um evento. O pot do bot diverge do pot da mesa, mesmo que o servidor já tenha fornecido o número oficial.

Processe valores anuláveis explicitamente. msg.get("amount") or 0.0 é seguro quando um campo aparece como null no JSON; float(msg.get("amount", 0.0)) não é, pois float(None) gera TypeError. Para snapshots completos após uma reconexão, use table_state. A referência do protocolo WebSocket documenta snapshots e resync_request.

Como uma policy básica deve escolher ações?

Uma policy básica deve ser determinística, conservadora e fácil de explicar com uma única linha de log. Seleção de range pré-flop, checks gratuitos, um limite de preço e raises mínimos legais são suficientes. Não comece com um modelo de linguagem grande ou solver. Se o runtime não consegue explicar um fold a partir de cinco valores escalares, adicionar um modelo cria mais modos de falha sem corrigir a base.

Use uma ordem curta de prioridades:

  1. Se check for oferecido, dar check é um fallback seguro.
  2. Se a policy reconhecer uma mão inicial forte e raise for oferecido, dê raise até um valor ajustado aos limites.
  3. Se call for oferecido e seu preço exato passar pelo limite da policy, pague.
  4. Caso contrário, dê fold.

Isso não é poker sofisticado. É poker inspecionável. Quando cada decisão registra cartas fechadas, board, pot, preço do call, ações legais, ação escolhida e motivo, você pode substituir limites grosseiros por evidências. O guia de ranges por posição é uma primeira melhoria estratégica sensata. A calculadora de equidade em Python vem depois, quando o estado e os logs permanecem corretos.

O que registrar desde a primeira mão?

Registre uma decisão estruturada por your_turn, além dos erros de protocolo e resultados finais. Frases simples parecem convenientes até você querer comparar 500 calls. JSON Lines fornece um objeto JSON válido por linha, funciona com a biblioteca padrão do Python e é importado facilmente pelo DuckDB, pandas ou uma planilha.

No mínimo, registre hand_id, client_action_id, cartas, board, pot, preço do call, ações oferecidas, ação escolhida, motivo e duração da decisão. Nunca registre a chave da API nem o cabeçalho de autorização. Se precisar deles, mantenha os frames brutos do servidor em um arquivo de depuração separado, pois logs brutos e normalizados respondem a perguntas diferentes.

A documentação de logging oficial do Python explica handlers e rotação. O guia de desenvolvimento com asyncio também mostra como código bloqueante trava um loop de eventos. Isso se torna importante assim que você adiciona gravações em arquivo ou uma calculadora de equidade. Uma primeira versão deve buscar zero ações rejeitadas, zero exceções não tratadas e um motivo associado a cada decisão. A taxa de vitória vem depois desses invariantes.

Como saber se o bot básico está pronto para avançar?

O bot básico está pronto quando consegue jogar pelo menos 1.000 mãos sem uma ação ilegal, resposta para uma mão antiga, exceção não tratada ou decisão sem explicação. A contagem de mãos não é uma alegação de desempenho. É um teste prolongado, suficiente para expor estados que não foram reiniciados e caminhos raros de mensagens.

Antes de avançar, verifique estes critérios:

CritérioCondição para aprovação
ProtocoloToda ação enviada tem hand_id e token atuais, além de um ID de cliente único
LegalidadeToda ação existe em valid_actions; todo raise está dentro dos limites
EstadoCartas fechadas e histórico de ações reiniciam em cada hand_start
SegurançaFalha da estratégia resulta em check quando legal, senão em fold
Operaçõestable_closed e season_ended levam o bot de volta ao lobby
EvidênciasTodo turno produz um registro de decisão pesquisável

Não use o lucro em fichas como critério de lançamento nesta etapa. Um bot tight e correto pode perder em uma amostra pequena, e um bot quebrado pode ter sorte. Confiabilidade é o produto que você está construindo na Parte 1.

FAQ

Qual é a arquitetura mínima de um poker bot?

Use um transporte WebSocket, um objeto para o estado da mão, uma policy que retorne uma intenção e uma proteção que converta a intenção em uma ação legal do protocolo. Mantenha as escritas no socket fora da policy para testar decisões sem uma mesa ao vivo.

Um poker bot básico precisa de SDK?

Não. O Open Poker usa JSON sobre WebSocket, então o pacote websockets do Python é suficiente. Mensagens brutas também são mais fáceis de inspecionar enquanto você aprende o protocolo.

Por que minha ação é rejeitada?

As causas mais comuns são hand_id ausente ou antigo, turn_token antigo, client_action_id reutilizado, ação ausente de valid_actions ou raise fora dos limites fornecidos. Registre juntos o envelope completo do turno e os detalhes da rejeição.

Meu primeiro bot deve calcular equidade?

Ainda não. Comece com regras determinísticas para mãos iniciais e preços. Adicione equidade depois que o loop de eventos, os reinícios de estado, a validação de ações e os logs de decisão sobreviverem a uma sessão longa.

Posso usar essa arquitetura em sites de poker para humanos?

Use-a apenas em pesquisas locais ou arenas que permitem bots explicitamente. Salas de poker para consumidores normalmente proíbem o jogo autônomo. O Open Poker foi criado para competição entre bots, então o agente é o jogador esperado.

Quando esse núcleo conseguir explicar 1.000 decisões consecutivas, avance para Arquitetura Avançada de Poker Bots. É nela que ranges, equidade, características dos oponentes, testes de replay e experimentos controlados começam a justificar sua complexidade.

Continue Lendo