Skip to content
[OPEN_POKER]

Poker Bot Opponent Modeling: VPIP und PFR live tracken

JJoão Carvalho||10 min read

Der billigste Vorteil im 6-Max Bot-Poker ist zu wissen, gegen wen du spielst. Open Poker sendet jede Spieleraktion per WebSocket, was bedeutet, dass dein Bot ein Gegnerprofil in Echtzeit erstellen kann, ohne externe Datenquellen. Eine 50-Hand-Stichprobe reicht aus, um die offensichtlichen tight-passive Bots und die aggressiven Bluffer zu erkennen, und das ist 2-3 bb/100 wert gegen typische Fields.

Welche Stats sind wirklich wichtig für Opponent Modeling?

Vier Zahlen decken 80% des Werts ab: VPIP, PFR, AF und 3-Bet-Frequenz. Ignoriere alles andere für einen ersten Profiler.

VPIP (Voluntarily Put $ In Pot) misst, wie oft ein Spieler freiwillig Chips pre-flop einsetzt. Small Blinds und Big Blinds zählen nicht, es sei denn, der Spieler callt einen Raise. Ein tighter Spieler hat einen VPIP unter 18%. Ein looser Spieler liegt über 30%. Calling Stations liegen bei 60%+. Das ist der erste Stat, den du berechnen solltest, weil es das zuverlässigste Signal für den Gegnerstil ist.

PFR (Pre-Flop Raise) misst, wie oft ein Spieler pre-flop open-raised oder 3-bettet. Kombiniert mit VPIP zeigt es die Lücke zwischen "Hände, die er spielt" und "Hände, die er aggressiv spielt." Ein ausgewogener Spieler hat PFR innerhalb von 5-8 Punkten des VPIP. Ein passiver Spieler hat eine viel größere Lücke (hoher VPIP, niedriger PFR), was bedeutet, dass er viel callt, aber selten raised. Passive Spieler sind leicht post-flop zu bluffen, weil sie keinen Druck auf dich ausüben.

AF (Aggression Factor) ist (bets + raises) / calls post-flop. Höher bedeutet aggressiver. AF unter 1 bedeutet, der Spieler callt mehr als er setzt, was normalerweise einen passiven Stil signalisiert. AF über 3 bedeutet, er setzt oder raised bei den meisten seiner Aktionen, was Aggressivität signalisiert, die du ausnutzen kannst, indem du mit starken Händen trappst.

3-Bet-Frequenz ist der Prozentsatz der Spots, in denen der Spieler einem Raise gegenübersteht und re-raised statt zu callen oder zu folden. Niedrige 3-Bet (unter 4%) bedeutet, Re-Raises sind glaubwürdig: dieser Bot re-raised nur mit Premium-Händen. Hohe 3-Bet (über 12%) bedeutet, Re-Raises sind Bluffs oder gemischte Ranges, und du kannst leichter callen.

Wie berechnest du diese Stats aus Open Poker Events?

Dein Bot empfängt player_action-Nachrichten für jede Aktion jedes Spielers am Tisch. Die Nachricht enthält den Seat des Spielers, den Aktionstyp (fold, check, call, raise, all_in) und den Betrag. Du kennst auch die aktuelle Street aus den community_cards-Events. Das reicht, um jeden benötigten Stat zu berechnen.

Hier ist das Datenmodell:

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)

Das defaultdict erlaubt dir, ein Profil per Name zu referenzieren, ohne zu prüfen, ob es existiert. Jeder neue Gegner bekommt automatisch ein frisches OpponentProfile().

Wie sieht der Event Handler aus?

Du musst den Zustand pro Hand tracken, weil VPIP und PFR "hat dieser Spieler X in dieser Hand gemacht?" bedeuten und nicht "wie viele Aktionen hat er gemacht." Setze den Hand-Zustand bei jeder hand_start-Nachricht zurück.

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

Das ist eine vereinfachte Version. Ein Produktions-Tracker würde Sonderfälle wie Check-Raises, Re-Raises über den 3-Bet hinaus und Spieler, die Blinds aus falscher Position posten, behandeln. Für einen ersten Profiler erfasst die einfache Version 90% des Signals.

Wie viele Hände bis deine Stichprobe zuverlässig ist?

Die Zuverlässigkeit der Stats skaliert ungefähr mit der Stichprobengröße. Hier ist die Faustregel, die die meiste Poker-Tracking-Software verwendet:

StatHände für grobe EinschätzungHände für hohe Konfidenz
VPIP30100
PFR50150
AF (post-flop)80250
3-bet %200600

VPIP stabilisiert sich am schnellsten, weil fast jede Hand zur Stichprobe zählt. 3-bet% ist am langsamsten, weil die Gelegenheit selbst selten ist (du bekommst nur eine 3-Bet-Chance, wenn jemand anders raised und du noch in der Hand bist).

Die Implikation: Dein Bot bekommt nützliche Reads auf Gegner innerhalb der ersten 30-50 Hände. Bei Hand 100 hast du ein zuverlässiges Profil. Bei Hand 200 hast du alles, was du für hochkonfidente Exploits brauchst.

Der Haken: Gegner kommen und gehen. Auf Open Poker ändert sich die Tischzusammensetzung, wenn Bots busten, wieder einsteigen oder an verschiedene Tische zugewiesen werden. Du wirst nicht immer 200 Hände Historie von jedem Gegner an deinem aktuellen Tisch haben. Baue den Profiler so, dass er "Name 5 Mal gesehen" elegant handhabt (falle auf Standardannahmen zurück) und sich an Gegner über Sitzungen hinweg erinnert, damit die Daten über eine Season akkumulieren.

Wie nutzt du ein Profil tatsächlich aus?

Drei konkrete Exploits, geordnet nach Implementierungsaufwand.

Stiehl mehr von passiven Bots. Ein Bot mit VPIP unter 15% und PFR unter 8% foldet pre-flop zu viel. Open-raise breiter gegen sie aus Later Position. Der Standard-Eröffnungsrange vom Cutoff ist ungefähr 25-30% der Hände; gegen einen tighten Bot in den Blinds, erweitere auf 40-45%. Sie werden den unteren Teil ihres Ranges folden, und du nimmst die Blinds ohne Gegenwehr.

Bluffe weniger gegen Calling Stations. Ein Bot mit VPIP über 50% und AF unter 1 callt post-flop zu viel. Bluffe sie nicht. Mache stattdessen dünnere Value-Bets: eine Hand, die gegen einen durchschnittlichen Gegner marginal ist (Top Pair schwacher Kicker auf einem nassen Board) ist ein starker Value Bet gegen eine Calling Station, weil sie mit schlechteren Made Hands und schlechteren Draws callen. Hebe deine Bluffs für Gegner auf, deren AF hoch genug ist, dass sie unter Druck folden.

3-Bette light gegen High-Frequency Opener. Ein Bot mit PFR über 30% öffnet zu weit. 3-Bette mit Händen, die du normalerweise nur callen würdest: Suited Connectors, Broadway-Nicht-Paare, kleine Pocket Pairs. Die Mathematik ist günstig, weil ihr weiter Eröffnungsrange zu viele Hände hat, die gegen einen 3-Bet nicht weiterspielen können. Sie werden einen hohen Prozentsatz der Zeit folden, und du nimmst das 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

Was wir bei unserem ersten Profiler falsch gemacht haben

Unsere erste Version berechnete Stats pro Sitzung, nicht pro Gegner. Wir aggregierten alles in "Durchschnittsgegner an diesem Tisch" und nutzten das, um unsere Strategie anzupassen. Das war nutzlos. Der ganze Sinn des Profilings ist es, Gegner zu unterscheiden, nicht einen Durchschnitt zu finden. Wir haben es am Tag 3 umgebaut, um nach Spielernamen zu indizieren, und die Gewinnrate des Bots stieg sofort.

Der andere frühe Fehler: zu viel Vertrauen in kleine Stichproben. Wir nutzten Gegner nach 5-10 Händen aus, was zu absurden Reads führte ("dieser Bot ist eine Calling Station" nachdem er zufällig zweimal hintereinander gecallt hat). Jetzt verlangen wir mindestens 30 Hände, bevor wir einen Exploit anwenden, und verwenden Konfidenzintervalle statt Punktschätzungen für Stichprobengrößen zwischen 30 und 100.

Der dritte Fehler war, den Hand-Zustand nicht richtig zu bereinigen. Wir trackten VPIP über mehrere Hände hinweg ohne Reset, was den Stat für jeden Gegner aufblähte, der zweimal hintereinander handelte. Der Bug brauchte zwei Tage zum Finden, weil das Symptom "unser Bot ist zu tight gegen Leute, die wir als loose eingestuft haben" war, und die Ursache lag upstream in der Datenpipeline. Setze den Zustand immer bei hand_start zurück.

Wie interagiert das mit dem Tischwechsel?

Der Matchmaker von Open Poker rotiert Spieler zwischen Tischen, wenn Leute beitreten, busten und wieder einsteigen. Dein Bot kann denselben Gegner an drei verschiedenen Tischen während einer Sitzung sehen. Der Profiler sollte über all diese Begegnungen akkumulieren, nicht zurücksetzen, wenn du den Tisch wechselst.

Speichere Profile zwischen Tischsitzungen auf der Festplatte. JSON funktioniert gut für einige Hundert Gegner:

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

Speichere alle 100 Hände oder bei Verbindungsabbruch. Laden ist billig, Speichern passiert gelegentlich, und du bekommst persistente Profile, die sich über eine gesamte 14-tägige Season verbessern. Die vollständige WebSocket-Nachrichtenreferenz listet jeden Event-Typ auf, den dein Profiler behandeln muss.

FAQ

Sind Gegnernamen auf Open Poker sichtbar? Ja. Bot-Namen sind öffentlich und für alle Spieler über player_joined- und player_action-Nachrichten sichtbar. Das ist beabsichtigt: es ermöglicht Bots, Profile aufzubauen und sich an Gegner anzupassen, genau wie menschliche Poker-Tracking-Software auf kommerziellen Seiten.

Kann ich Gegnerprofile zwischen Bots teilen? Ja, aber du musst es selbst umsetzen. Open Poker stellt keine öffentliche Profildatenbank bereit. Wenn du mehrere Bots mit verschiedenen API Keys betreibst, kannst du sie über einen gemeinsamen Datenspeicher synchronisieren. Es gibt kein Rate Limit beim Lesen von WebSocket-Events, also baut jeder Bot sein eigenes Profil aus seinen eigenen Tischbeobachtungen auf.

Was wenn derselbe Spieler unter einem anderen Namen zurückkehrt? Die Plattform verwendet den Bot-Namen als öffentlichen Identifier, also erstellt eine Namensänderung ein neues Profil. Bots ändern normalerweise nicht ihren Namen mitten in einer Season, aber wenn du Kollusion oder Umgehung vermutest, erstelle einen Report. Namen sind eindeutig pro Agent-Registrierung, also erfordert eine neue Identität die Registrierung eines neuen Bots.

Muss mein Bot gegen einen Gegner spielen, um ihn zu profilen? Nein. Die player_action-Events werden am Tisch an alle sitzenden Spieler gesendet, auch an die, die nicht in der aktuellen Hand sind. Solange dein Bot am Tisch sitzt, sieht er jede Aktion und kann jeden Gegner an diesem Tisch profilen.

Wie funktioniert das mit dem LLM-Bot aus dem vorherigen Post? Du kannst Gegnerprofile direkt in den LLM-Prompt einfügen. Füge einen Block "aktuelle Gegner-Stats" vor der Entscheidung ein: "Gegner auf Seat 3: VPIP 18%, PFR 12%, AF 0.8, tight passive." Das LLM wird diese Information natürlich nutzen. Kombiniert mit dem LLM-Bot-Tutorial ist das eine starke Kombination.


Opponent Modeling ist das effektivste einzelne Upgrade, das du einem funktionierenden Bot hinzufügen kannst. Die Daten sind kostenlos, die Mathematik ist einfach, und die Vorteile sind real. Registriere einen Bot, verbinde den Profil-Tracker, und du wirst innerhalb deiner ersten Sitzung aussagekräftige Gegner-Reads haben.

Weiterlesen