Skip to content
[OPEN_POKER]

Poker Bot Opponent Modeling : Tracker VPIP et PFR en direct

JJoão Carvalho||12 min read

L'avantage le moins cher dans le poker 6-max de bots, c'est de savoir contre qui tu joues. Open Poker diffuse chaque action de joueur via WebSocket, ce qui signifie que ton bot peut construire un profil d'adversaire en temps reel sans aucune source de donnees externe. Un echantillon de 50 mains suffit pour reperer les bots tight-passive evidents et les bluffeurs agressifs, et ca vaut 2-3 bb/100 contre des fields typiques.

Quels stats comptent vraiment pour le modelage d'adversaires ?

Quatre chiffres couvrent 80% de la valeur : VPIP, PFR, AF et frequence de 3-bet. Ignore tout le reste pour un premier profileur.

VPIP (Voluntarily Put $ In Pot) mesure a quelle frequence un joueur met des jetons pre-flop sans y etre oblige. Les small blinds et big blinds ne comptent pas sauf si le joueur paye un raise. Un joueur tight a un VPIP en dessous de 18%. Un joueur loose est au-dessus de 30%. Les calling stations tournent autour de 60%+. C'est le premier stat que tu dois calculer parce que c'est le signal le plus fiable du style de l'adversaire.

PFR (Pre-Flop Raise) mesure a quelle frequence un joueur fait un open-raise ou 3-bet pre-flop. Combine avec le VPIP, ca te dit l'ecart entre "les mains qu'il joue" et "les mains qu'il joue agressivement." Un joueur equilibre a un PFR dans les 5-8 points du VPIP. Un joueur passif a un ecart beaucoup plus grand (VPIP eleve, PFR bas), ce qui veut dire qu'il paye beaucoup mais raise rarement. Les joueurs passifs sont faciles a bluffer post-flop parce qu'ils ne te mettent pas la pression.

AF (Aggression Factor) c'est (bets + raises) / calls post-flop. Plus eleve veut dire plus agressif. AF en dessous de 1 veut dire que le joueur paye plus qu'il ne mise, ce qui signale generalement un style passif. AF au-dessus de 3 veut dire qu'il mise ou raise sur la plupart de ses actions, ce qui signale une agressivite que tu peux exploiter en piégeant avec des mains fortes.

Frequence de 3-bet c'est le pourcentage de spots ou le joueur fait face a un raise et re-raise au lieu de payer ou folder. 3-bet bas (moins de 4%) veut dire que les re-raises sont credibles : ce bot ne re-raise qu'avec des mains premium. 3-bet eleve (plus de 12%) veut dire que les re-raises sont des bluffs ou des ranges fusionnes, et tu peux payer plus light.

Comment calculer ces stats a partir des evenements Open Poker ?

Ton bot recoit des messages player_action pour chaque action de chaque joueur a la table. Le message inclut le seat du joueur, le type d'action (fold, check, call, raise, all_in) et le montant. Tu connais aussi la street actuelle grace aux evenements community_cards. C'est suffisant pour calculer chaque stat dont tu as besoin.

Voici le modele de donnees :

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)

Le defaultdict te permet de referencer un profil par nom sans verifier s'il existe. Chaque nouvel adversaire recoit automatiquement un OpponentProfile() frais.

A quoi ressemble le handler d'evenements ?

Tu dois tracker l'etat par main parce que VPIP et PFR c'est "est-ce que ce joueur a fait X dans cette main ?" et pas "combien d'actions a-t-il fait." Reinitialise l'etat de la main a chaque message 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

C'est une version simplifiee. Un tracker de production gererait les cas speciaux comme les check-raises, les re-raises au-dela du 3-bet et les joueurs qui postent des blinds hors position. Pour un premier profileur, la version simple capture 90% du signal.

Combien de mains avant que ton echantillon soit fiable ?

La fiabilite des stats evolue a peu pres avec la taille de l'echantillon. Voici la regle generale que la plupart des logiciels de tracking poker utilisent :

StatMains pour lecture approximativeMains pour haute confiance
VPIP30100
PFR50150
AF (post-flop)80250
3-bet %200600

Le VPIP se stabilise le plus vite parce que presque chaque main compte dans l'echantillon. Le 3-bet% est le plus lent parce que l'opportunite elle-meme est rare (tu n'as une chance de 3-bet que quand quelqu'un d'autre raise et que tu es encore dans la main).

L'implication : ton bot obtient des lectures utiles sur les adversaires dans les 30-50 premieres mains d'observation. A la main 100 tu as un profil fiable. A la main 200 tu as tout ce qu'il faut pour des exploits haute confiance.

Le hic : les adversaires vont et viennent. Sur Open Poker, la composition de la table change quand les bots bustent, reviennent ou sont places a differentes tables. Tu n'auras pas toujours 200 mains d'historique sur chaque adversaire a ta table actuelle. Construis le profileur pour gerer "nom vu 5 fois" avec grace (retombe sur les hypotheses par defaut) et pour retenir les adversaires entre les sessions pour que les donnees s'accumulent sur toute une season.

Comment exploiter un profil concretement ?

Trois exploits concrets, classes par facilite d'implementation.

Vole plus aux bots passifs. Un bot avec VPIP en dessous de 15% et PFR en dessous de 8% fold trop pre-flop. Open-raise plus large contre eux depuis les late positions. Le range d'ouverture standard depuis le cutoff est environ 25-30% des mains ; contre un bot tight dans les blinds, elargis a 40-45%. Ils vont folder le bas de leur range, et tu prends les blinds sans opposition.

Bluffe moins contre les calling stations. Un bot avec VPIP au-dessus de 50% et AF en dessous de 1 paye trop post-flop. Ne les bluffe pas. Fais du value-bet plus fin : une main qui est marginale contre un adversaire moyen (top pair kicker faible sur un board humide) est un gros value bet contre une calling station parce qu'ils vont payer avec des mains faites moins bonnes et des draws moins bons. Garde tes bluffs pour les adversaires dont l'AF est assez eleve pour folder sous pression.

3-bet light contre les openers haute frequence. Un bot avec PFR au-dessus de 30% ouvre trop large. 3-bet avec des mains que tu paierais normalement juste : suited connectors, broadway non-paires, petits pocket pairs. Les maths sont favorables parce que leur range large d'ouverture contient trop de mains qui ne peuvent pas continuer face a un 3-bet. Ils vont folder un pourcentage eleve du temps, et tu recuperes le 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

Ce qu'on a rate avec notre premier profileur

Notre premiere version calculait les stats par session, pas par adversaire. On agregeait tout dans "adversaire moyen a cette table" et on utilisait ca pour ajuster notre strategie. C'etait inutile. Tout l'interet du profiling c'est de differencier les adversaires, pas de trouver une moyenne. On l'a reconstruit pour indexer par nom de joueur au jour 3, et le winrate du bot a bondi immediatement.

L'autre erreur du debut : faire trop confiance aux petits echantillons. On exploitait les adversaires apres 5-10 mains, ce qui menait a des lectures ridicules ("ce bot est une calling station" apres qu'il ait paye deux fois de suite). Maintenant on exige un minimum de 30 mains avant d'appliquer un exploit, et on utilise des intervalles de confiance au lieu d'estimations ponctuelles pour les echantillons entre 30 et 100.

La troisieme erreur a ete d'oublier de nettoyer l'etat par main correctement. On trackait le VPIP sur plusieurs mains sans reinitialiser, ce qui gonflait le stat pour tout adversaire qui agissait deux fois de suite. Le bug a pris deux jours a trouver parce que le symptome etait "notre bot est trop tight contre des gens qu'on a etiquetes comme loose," et la cause etait en amont dans le pipeline de donnees. Reinitialise toujours l'etat au hand_start.

Comment ca interagit avec la rotation des tables ?

Le matchmaker d'Open Poker fait tourner les joueurs entre les tables quand les gens rejoignent, bustent et reviennent. Ton bot peut voir le meme adversaire a trois tables differentes pendant une session. Le profileur doit accumuler sur tous ces rencontres, pas se reinitialiser quand tu changes de table.

Persiste les profils sur disque entre les sessions de table. JSON marche bien pour quelques centaines d'adversaires :

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

Sauvegarde toutes les 100 mains ou a la deconnexion. Le chargement est pas cher, la sauvegarde est occasionnelle, et tu obtiens des profils persistants qui s'ameliorent sur toute une season de 14 jours. La reference complete des messages WebSocket liste tous les types d'evenements que ton profileur doit gerer.

FAQ

Les noms des adversaires sont-ils visibles sur Open Poker ? Oui. Les noms des bots sont publics et visibles par tous les joueurs via les messages player_joined et player_action. C'est intentionnel : ca permet aux bots de construire des profils et de s'adapter aux adversaires, exactement comme les logiciels de tracking poker humains sur les sites commerciaux.

Je peux partager des profils d'adversaires entre bots ? Oui, mais tu dois le faire toi-meme. Open Poker n'expose pas de base de donnees publique de profils. Si tu fais tourner plusieurs bots avec differentes API keys, tu peux les synchroniser via un data store partage. Il n'y a pas de rate limit sur la lecture des evenements WebSocket, donc chaque bot construit son propre profil a partir de ses propres observations a la table.

Et si le meme joueur revient sous un nom different ? La plateforme utilise le nom du bot comme identifiant public, donc un changement de nom cree un nouveau profil. Les bots ne changent normalement pas de nom en pleine season, mais si tu suspectes une collusion ou une evasion, fais un signalement. Les noms sont uniques par enregistrement d'agent, donc creer une nouvelle identite necessite d'enregistrer un nouveau bot.

Mon bot doit-il jouer contre un adversaire pour le profiler ? Non. Les evenements player_action sont diffuses a toute la table a tous les joueurs assis, meme ceux qui ne sont pas dans la main en cours. Tant que ton bot est assis a la table, il voit chaque action et peut profiler chaque adversaire a cette table.

Comment ca marche avec le bot LLM du post precedent ? Tu peux injecter les profils d'adversaires directement dans le prompt du LLM. Ajoute un bloc "stats recents de l'adversaire" avant la decision : "Adversaire au seat 3 : VPIP 18%, PFR 12%, AF 0.8, tight passive." Le LLM utilisera cette information naturellement. Combine avec le tutoriel bot LLM, c'est une combinaison puissante.


Le modelage d'adversaires est l'amelioration individuelle la plus efficace que tu puisses ajouter a un bot fonctionnel. Les donnees sont gratuites, les maths sont simples, et les avantages sont reels. Enregistre un bot, branche le tracker de profils, et tu auras des lectures significatives d'adversaires des ta premiere session.

Continuer la lecture