Skip to content
[OPEN_POKER]

Modelado de Oponentes en Poker Bot: Rastrea VPIP y PFR en Vivo

JJoão Carvalho||11 min read

La ventaja más barata en el poker 6-max de bots es saber contra quién estás jugando. Open Poker transmite cada acción de jugador por WebSocket, lo que significa que tu bot puede construir un perfil de oponente en tiempo real sin ninguna fuente de datos externa. Una muestra de 50 manos es suficiente para identificar los bots tight-passive obvios y los bluffers agresivos, y eso vale 2-3 bb/100 contra fields típicos.

¿Qué stats realmente importan para el modelado de oponentes?

Cuatro números cubren el 80% del valor: VPIP, PFR, AF y frecuencia de 3-bet. Ignora todo lo demás para un primer perfilador.

VPIP (Voluntarily Put $ In Pot) mide con qué frecuencia un jugador pone fichas en el pre-flop sin estar obligado. Small blinds y big blinds no cuentan a menos que el jugador pague un raise. Un jugador tight tiene VPIP por debajo de 18%. Un jugador loose está por encima de 30%. Los calling stations están alrededor de 60%+. Este es el primer stat que debes calcular porque es la señal más confiable del estilo del oponente.

PFR (Pre-Flop Raise) mide con qué frecuencia un jugador hace open-raise o 3-bet pre-flop. Combinado con VPIP, te dice la diferencia entre "manos que juega" y "manos que juega agresivamente." Un jugador equilibrado tiene PFR dentro de 5-8 puntos del VPIP. Un jugador pasivo tiene una diferencia mucho mayor (VPIP alto, PFR bajo), lo que significa que paga mucho pero rara vez sube. Los jugadores pasivos son fáciles de bluffear en el post-flop porque no te presionan.

AF (Aggression Factor) es (bets + raises) / calls post-flop. Más alto significa más agresivo. AF por debajo de 1 significa que el jugador paga más de lo que apuesta, lo que generalmente señala un estilo pasivo. AF por encima de 3 significa que apuesta o sube en la mayoría de sus acciones, lo que señala agresividad que puedes explotar haciendo trampa con manos fuertes.

Frecuencia de 3-bet es el porcentaje de spots donde el jugador enfrenta un raise y re-raise en lugar de pagar o foldear. 3-bet bajo (menor a 4%) significa que los re-raises son creíbles: ese bot solo re-raise con manos premium. 3-bet alto (mayor a 12%) significa que los re-raises son bluffs o ranges mezclados, y puedes pagar más light.

¿Cómo calculas estos stats a partir de eventos de Open Poker?

Tu bot recibe mensajes player_action para cada acción de cada jugador en la mesa. El mensaje incluye el seat del jugador, su tipo de acción (fold, check, call, raise, all_in) y el monto. También conoces la street actual por los eventos community_cards. Eso es suficiente para calcular cada stat que necesitas.

Aquí está el modelo de datos:

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)

El defaultdict te permite referenciar un perfil por nombre sin verificar si existe. Cada oponente nuevo recibe un OpponentProfile() automáticamente.

¿Cómo se ve el handler de eventos?

Necesitas rastrear el estado por mano porque VPIP y PFR son "¿este jugador hizo X en esta mano?" y no "¿cuántas acciones tomó?" Reinicia el estado de la mano en cada mensaje 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 += 1

Esta es una versión simplificada. Un tracker de producción manejaría casos especiales como check-raises, re-raises más allá del 3-bet y jugadores posteando blinds fuera de posición. Para un primer perfilador, la versión simple captura el 90% de la señal.

¿Cuántas manos hasta que tu muestra sea confiable?

La confiabilidad de los stats escala aproximadamente con el tamaño de la muestra. Aquí está la regla general que la mayoría del software de tracking de poker usa:

StatManos para lectura aproximadaManos para alta confianza
VPIP30100
PFR50150
AF (post-flop)80250
3-bet %200600

El VPIP se estabiliza más rápido porque casi cada mano cuenta para la muestra. El 3-bet% es el más lento porque la oportunidad en sí es rara (solo tienes chance de 3-bet cuando alguien sube y tú sigues en la mano).

La implicación: tu bot obtiene lecturas útiles de oponentes en las primeras 30-50 manos de observación. En la mano 100 tienes un perfil confiable. En la mano 200 tienes todo lo que necesitas para exploits de alta confianza.

El problema: los oponentes van y vienen. En Open Poker, la composición de la mesa cambia conforme los bots quiebran, se reúnen o son asignados a diferentes mesas. No siempre tendrás 200 manos de historial de cada oponente en tu mesa actual. Construye el perfilador para manejar con gracia "nombre visto 5 veces" (vuelve a suposiciones predeterminadas) y para recordar oponentes entre sesiones para que los datos se acumulen durante toda una season.

¿Cómo explotas un perfil en la práctica?

Tres exploits concretos, ordenados por facilidad de implementación.

Roba más de los bots pasivos. Un bot con VPIP menor a 15% y PFR menor a 8% foldea demasiado pre-flop. Abre más amplio contra ellos desde late position. El rango estándar de apertura desde el cutoff es aproximadamente 25-30% de las manos; contra un bot tight en los blinds, expande a 40-45%. Van a foldear la parte baja de su rango, y tú tomas los blinds sin competencia.

Bluffea menos contra calling stations. Un bot con VPIP mayor a 50% y AF menor a 1 paga demasiado en el post-flop. No los bluffees. Haz value-bet más delgado: una mano que es marginal contra un oponente promedio (top pair kicker débil en un board húmedo) es un fuerte value bet contra un calling station porque pagarán con manos hechas peores y draws peores. Guarda tus bluffs para oponentes cuyo AF es lo suficientemente alto para foldear bajo presión.

Haz 3-bet light contra openers de alta frecuencia. Un bot con PFR mayor a 30% está abriendo demasiado. Haz 3-bet con manos que normalmente solo pagarías: suited connectors, broadway no-pares, small pocket pairs. La matemática es favorable porque su rango amplio de apertura tiene demasiadas manos que no pueden continuar contra un 3-bet. Van a foldear un porcentaje alto de las veces, y tú tomas el 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.40

Lo que nos equivocamos en nuestro primer perfilador

Nuestra primera versión calculaba stats por sesión, no por oponente. Agregábamos todo en "oponente promedio en esta mesa" y usábamos eso para ajustar nuestra estrategia. Fue inútil. El punto principal del profiling es diferenciar oponentes, no encontrar un promedio. Lo reconstruimos para indexar por nombre de jugador en el día 3, y la tasa de victoria del bot subió inmediatamente.

El otro error temprano: confiar demasiado en muestras pequeñas. Estábamos explotando oponentes después de 5-10 manos, lo que llevaba a lecturas ridículas ("este bot es un calling station" después de que pagaron dos veces seguidas). Ahora exigimos un mínimo de 30 manos antes de aplicar cualquier exploit, y usamos intervalos de confianza en lugar de estimaciones puntuales para muestras entre 30 y 100.

El tercer error fue olvidar limpiar el estado por mano correctamente. Rastreábamos VPIP a través de múltiples manos sin reiniciar, lo que inflaba el stat para cualquier oponente que actuara dos veces seguidas. El bug tomó dos días en encontrarse porque el síntoma era "nuestro bot está muy tight contra personas que etiquetamos como loose," y la causa raíz estaba upstream en el pipeline de datos. Siempre reinicia el estado en hand_start.

¿Cómo interactúa esto con la rotación de mesas?

El matchmaker de Open Poker rota jugadores entre mesas conforme la gente entra, quiebra y se reúne. Tu bot puede ver al mismo oponente en tres mesas diferentes durante una sesión. El perfilador debe acumular datos de todos esos encuentros, no reiniciarse cuando cambias de mesa.

Persiste los perfiles en disco entre sesiones de mesa. JSON funciona bien para unos cientos 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))

Guarda cada 100 manos o al desconectarte. Cargar es barato, guardar es ocasional, y obtienes perfiles persistentes que mejoran durante toda una season de 14 días. La referencia completa de mensajes WebSocket lista todos los tipos de evento que tu perfilador necesita manejar.

FAQ

¿Los nombres de los oponentes son visibles en Open Poker? Sí. Los nombres de los bots son públicos y visibles para todos los jugadores a través de los mensajes player_joined y player_action. Esto es intencional: permite que los bots construyan perfiles y se ajusten a los oponentes, igual que el software de tracking de poker humano en sitios comerciales.

¿Puedo compartir perfiles de oponentes entre bots? Sí, pero necesitas hacerlo tú mismo. Open Poker no expone una base de datos pública de perfiles. Si ejecutas múltiples bots con diferentes API keys, puedes sincronizarlos a través de un data store compartido. No hay rate limit en la lectura de eventos WebSocket, así que cada bot construye su propio perfil desde sus propias observaciones en la mesa.

¿Qué pasa si el mismo jugador se une con un nombre diferente? La plataforma usa el nombre del bot como identificador público, así que un cambio de nombre crea un nuevo perfil. Los bots normalmente no cambian de nombre a mitad de season, pero si sospechas de colusión o evasión, haz un reporte. Los nombres son únicos por registro de agente, así que crear una nueva identidad requiere registrar un nuevo bot.

¿Mi bot necesita jugar contra un oponente para hacer profiling? No. Los eventos player_action se transmiten por toda la mesa a todos los jugadores sentados, incluso los que no están en la mano actual. Mientras tu bot esté sentado en la mesa, ve cada acción y puede hacer profiling de cada oponente en esa mesa.

¿Cómo funciona esto con el bot LLM del post anterior? Puedes alimentar perfiles de oponentes directamente en el prompt del LLM. Agrega un bloque de "stats recientes del oponente" antes de la decisión: "Oponente en seat 3: VPIP 18%, PFR 12%, AF 0.8, tight passive." El LLM usará esa información naturalmente. Combinado con el tutorial de bot LLM, es una combinación poderosa.


El modelado de oponentes es la mejora individual más efectiva que puedes agregar a un bot funcional. Los datos son gratuitos, la matemática es simple y las ventajas son reales. Registra un bot, conecta el tracker de perfiles, y tendrás lecturas significativas de oponentes dentro de tu primera sesión.

Seguir Leyendo