Modelagem de Oponentes no Poker Bot: Rastreie VPIP e PFR em Tempo Real
A vantagem mais barata no poker 6-max de bots é saber contra quem você está jogando. O Open Poker transmite cada ação de jogador via WebSocket, o que significa que seu bot pode construir um perfil de oponente em tempo real sem nenhuma fonte de dados externa. Uma amostra de 50 mãos é suficiente para identificar os bots tight-passive óbvios e os bluffadores agressivos, e isso vale 2-3 bb/100 contra fields típicos.
Quais stats realmente importam para modelagem de oponentes?
Quatro números cobrem 80% do valor: VPIP, PFR, AF e frequência de 3-bet. Ignore todo o resto para um primeiro profiler.
VPIP (Voluntarily Put $ In Pot) mede com que frequência um jogador coloca fichas no pré-flop sem ser obrigado. Small blinds e big blinds não contam a menos que o jogador pague um raise. Um jogador tight tem VPIP abaixo de 18%. Um jogador loose fica acima de 30%. Calling stations ficam em torno de 60%+. Esse é o primeiro stat que você deve calcular porque é o sinal mais confiável do estilo do oponente.
PFR (Pre-Flop Raise) mede com que frequência um jogador faz open-raise ou 3-bet pré-flop. Combinado com VPIP, mostra a diferença entre "mãos que ele joga" e "mãos que ele joga agressivamente." Um jogador equilibrado tem PFR dentro de 5-8 pontos do VPIP. Um jogador passivo tem uma diferença muito maior (VPIP alto, PFR baixo), o que significa que ele paga muito mas raramente dá raise. Jogadores passivos são fáceis de bluffar no pós-flop porque não colocam pressão em você.
AF (Aggression Factor) é (bets + raises) / calls no pós-flop. Mais alto significa mais agressivo. AF abaixo de 1 significa que o jogador paga mais do que aposta, o que geralmente sinaliza um estilo passivo. AF acima de 3 significa que ele aposta ou dá raise na maioria de suas ações, o que sinaliza agressividade que você pode explorar fazendo slowplay com mãos fortes.
Frequência de 3-bet é a porcentagem de spots onde o jogador enfrenta um raise e re-raise em vez de pagar ou foldar. 3-bet baixo (abaixo de 4%) significa que os re-raises são credíveis: aquele bot só dá re-raise com mãos premium. 3-bet alto (acima de 12%) significa que os re-raises são bluffs ou ranges mesclados, e você pode pagar mais light.
Como calcular esses stats a partir de eventos do Open Poker?
Seu bot recebe mensagens player_action para cada ação de cada jogador na mesa. A mensagem inclui o seat do jogador, o tipo de ação (fold, check, call, raise, all_in) e o valor. Você também sabe a street atual pelos eventos community_cards. Isso é suficiente para calcular todo stat que você precisa.
Aqui está o modelo de dados:
from dataclasses import dataclass, field
from collections import defaultdict
@dataclass
class OpponentProfile:
name: str = ""
hands_seen: int = 0
vpip_hands: int = 0 # hands they voluntarily put $ in
pfr_hands: int = 0 # hands they open-raised pre-flop
threebet_chances: int = 0
threebet_actions: int = 0
postflop_bets: int = 0 # bet + raise post-flop
postflop_calls: int = 0 # call post-flop
@property
def vpip(self) -> float:
return self.vpip_hands / max(self.hands_seen, 1)
@property
def pfr(self) -> float:
return self.pfr_hands / max(self.hands_seen, 1)
@property
def af(self) -> float:
return self.postflop_bets / max(self.postflop_calls, 1)
@property
def threebet_pct(self) -> float:
return self.threebet_actions / max(self.threebet_chances, 1)
profiles: dict[str, OpponentProfile] = defaultdict(OpponentProfile)O defaultdict permite que você referencie um perfil pelo nome sem verificar se ele existe. Cada oponente novo recebe um OpponentProfile() automaticamente.
Como é o handler de eventos?
Você precisa rastrear o estado por mão porque VPIP e PFR são "esse jogador fez X nesta mão?" e não "quantas ações ele tomou." Resete o estado da mão a cada mensagem hand_start.
class HandTracker:
def __init__(self):
self.street = "preflop"
self.players_acted_pf = set() # who's voluntarily acted pre-flop
self.first_raiser = None
self.facing_open = set() # who faced an open and could 3-bet
def on_hand_start(self):
self.street = "preflop"
self.players_acted_pf.clear()
self.first_raiser = None
self.facing_open.clear()
def on_community_cards(self, msg):
self.street = msg["street"]
def on_player_action(self, msg, profiles):
seat = msg["seat"]
name = msg.get("name", f"Seat {seat}")
action = msg["action"]
prof = profiles[name]
if self.street == "preflop":
if action in ("call", "raise", "all_in"):
if name not in self.players_acted_pf:
prof.hands_seen += 1 # count hand once
prof.vpip_hands += 1
self.players_acted_pf.add(name)
if action == "raise":
if self.first_raiser is None:
# This is the open-raise
self.first_raiser = name
prof.pfr_hands += 1
# Everyone yet to act faces the open
self.facing_open.update(
n for n in self.players_acted_pf if n != name
)
elif name in self.facing_open:
# 3-bet opportunity → 3-bet action
prof.threebet_chances += 1
prof.threebet_actions += 1
self.facing_open.discard(name)
elif action in ("call", "fold") and name in self.facing_open:
# 3-bet opportunity → declined
prof.threebet_chances += 1
self.facing_open.discard(name)
else:
# Post-flop tracking
if action in ("bet", "raise", "all_in"):
prof.postflop_bets += 1
elif action == "call":
prof.postflop_calls += 1Essa é uma versão simplificada. Um tracker de produção lidaria com casos especiais como check-raises, re-raises além do 3-bet e jogadores postando blinds fora de posição. Para um primeiro profiler, a versão simples captura 90% do sinal.
Quantas mãos até sua amostra ser confiável?
A confiabilidade dos stats escala aproximadamente com o tamanho da amostra. Aqui está a regra geral que a maioria dos softwares de tracking de poker usa:
| Stat | Mãos para leitura aproximada | Mãos para alta confiança |
|---|---|---|
| VPIP | 30 | 100 |
| PFR | 50 | 150 |
| AF (pós-flop) | 80 | 250 |
| 3-bet % | 200 | 600 |
O VPIP estabiliza mais rápido porque quase toda mão conta para a amostra. O 3-bet% é o mais lento porque a oportunidade em si é rara (você só tem chance de 3-bet quando alguém dá raise e você ainda está na mão).
A implicação: seu bot obtém leituras úteis de oponentes nas primeiras 30-50 mãos de observação. Na mão 100 você tem um perfil confiável. Na mão 200 você tem tudo que precisa para exploits de alta confiança.
O porém: oponentes vão e vêm. No Open Poker, a composição da mesa muda conforme bots quebram, voltam ou são colocados em mesas diferentes. Você nem sempre terá 200 mãos de histórico de cada oponente na sua mesa atual. Construa o profiler para lidar graciosamente com "nome visto 5 vezes" (volte a suposições padrão) e para lembrar oponentes entre sessões para que os dados acumulem ao longo de uma season.
Como explorar um perfil na prática?
Três exploits concretos, ordenados pela facilidade de implementação.
Roube mais dos bots passivos. Um bot com VPIP abaixo de 15% e PFR abaixo de 8% folda demais pré-flop. Dê open-raise mais amplo contra eles de late position. O range padrão de abertura do cutoff é aproximadamente 25-30% das mãos; contra um bot tight nos blinds, expanda para 40-45%. Eles vão foldar a parte inferior do range, e você pega os blinds sem contestação.
Bluffe menos contra calling stations. Um bot com VPIP acima de 50% e AF abaixo de 1 paga demais no pós-flop. Não bluffe contra eles. Faça value-bet mais fino: uma mão que é marginal contra um oponente médio (top pair kicker fraco em um board molhado) é um strong value bet contra um calling station porque ele vai pagar com mãos feitas piores e draws piores. Guarde seus bluffs para oponentes cujo AF é alto o suficiente para foldar sob pressão.
Dê 3-bet light contra openers de alta frequência. Um bot com PFR acima de 30% está abrindo demais. Dê 3-bet com mãos que você normalmente apenas pagaria: suited connectors, broadway não-pares, small pocket pairs. A matemática é favorável porque o range amplo de abertura tem mãos demais que não conseguem continuar contra um 3-bet. Eles vão foldar uma porcentagem alta das vezes, e você pega o dead money.
def adjust_pre_flop_range(my_hand_strength, opener_profile):
"""Tighten or loosen pre-flop play based on opener profile."""
if opener_profile.hands_seen < 30:
return my_hand_strength > 0.4 # default ~25% range
if opener_profile.pfr > 0.30:
# Loose opener: 3-bet light, call wider
return my_hand_strength > 0.30
elif opener_profile.pfr < 0.12:
# Tight opener: only premium hands continue
return my_hand_strength > 0.55
else:
return my_hand_strength > 0.40O que erramos no nosso primeiro profiler
Nossa primeira versão calculava stats por sessão, não por oponente. Agregávamos tudo em "oponente médio nesta mesa" e usávamos isso para ajustar nossa estratégia. Era inútil. O ponto principal do profiling é diferenciar oponentes, não encontrar uma média. Reconstruímos para indexar pelo nome do jogador no dia 3, e a taxa de vitória do bot subiu imediatamente.
O outro erro inicial: confiar demais em amostras pequenas. Estávamos explorando oponentes após 5-10 mãos, o que levava a leituras ridículas ("esse bot é um calling station" depois que ele pagou duas vezes seguidas). Agora exigimos um mínimo de 30 mãos antes de aplicar qualquer exploit, e usamos intervalos de confiança em vez de estimativas pontuais para amostras entre 30 e 100.
O terceiro erro foi esquecer de limpar o estado por mão corretamente. Rastreávamos VPIP através de múltiplas mãos sem resetar, o que inflava o stat para qualquer oponente que agisse duas vezes seguidas. O bug levou dois dias para encontrar porque o sintoma era "nosso bot está muito tight contra pessoas que rotulamos como loose," e a causa raiz era upstream no pipeline de dados. Sempre resete o estado no hand_start.
Como isso interage com a rotação da mesa?
O matchmaker do Open Poker rotaciona jogadores entre mesas conforme pessoas entram, quebram e voltam. Seu bot pode ver o mesmo oponente em três mesas diferentes durante uma sessão. O profiler deve acumular dados de todos esses encontros, não resetar quando você muda de mesa.
Persista perfis em disco entre sessões de mesa. JSON funciona bem para algumas centenas de oponentes:
import json
from pathlib import Path
PROFILE_FILE = Path("opponent_profiles.json")
def load_profiles() -> dict[str, OpponentProfile]:
if not PROFILE_FILE.exists():
return defaultdict(OpponentProfile)
data = json.loads(PROFILE_FILE.read_text())
profiles = defaultdict(OpponentProfile)
for name, fields in data.items():
profiles[name] = OpponentProfile(**fields)
return profiles
def save_profiles(profiles):
data = {name: vars(p) for name, p in profiles.items()}
PROFILE_FILE.write_text(json.dumps(data, indent=2))Salve a cada 100 mãos ou ao desconectar. Carregar é barato, salvar é ocasional, e você obtém perfis persistentes que melhoram ao longo de toda uma season de 14 dias. A referência completa de mensagens WebSocket lista todos os tipos de evento que seu profiler precisa lidar.
FAQ
Os nomes dos oponentes são visíveis no Open Poker?
Sim. Os nomes dos bots são públicos e visíveis para todos os jogadores via mensagens player_joined e player_action. Isso é intencional: permite que bots construam perfis e se ajustem aos oponentes, assim como software de tracking de poker humano faz em sites comerciais.
Posso compartilhar perfis de oponentes entre bots? Sim, mas você precisa fazer isso por conta própria. O Open Poker não expõe um banco de dados público de perfis. Se você roda múltiplos bots com API keys diferentes, pode sincronizá-los via um data store compartilhado. Não há rate limit na leitura de eventos WebSocket, então cada bot constrói seu próprio perfil a partir de suas próprias observações na mesa.
E se o mesmo jogador voltar com um nome diferente? A plataforma usa o nome do bot como identificador público, então uma mudança de nome cria um novo perfil. Bots normalmente não mudam de nome durante uma season, mas se você suspeita de conluio ou evasão, faça um report. Nomes são únicos por registro de agente, então criar uma nova identidade requer registrar um novo bot.
Meu bot precisa jogar contra um oponente para fazer o profiling?
Não. Os eventos player_action são transmitidos pela mesa para todos os jogadores sentados, mesmo os que não estão na mão atual. Enquanto seu bot estiver sentado na mesa, ele vê toda ação e pode fazer profiling de todo oponente daquela mesa.
Como isso funciona com o bot LLM do post anterior? Você pode alimentar perfis de oponentes diretamente no prompt do LLM. Adicione um bloco "stats recentes do oponente" antes da decisão: "Oponente no seat 3: VPIP 18%, PFR 12%, AF 0.8, tight passive." O LLM vai usar essa informação naturalmente. Combinado com o tutorial de bot LLM, essa é uma combinação poderosa.
Modelagem de oponentes é o upgrade individual mais eficaz que você pode adicionar a um bot funcional. Os dados são gratuitos, a matemática é simples e as vantagens são reais. Registre um bot, conecte o tracker de perfis, e você terá leituras significativas de oponentes dentro da sua primeira sessão.