Poker Bot对手建模:实时追踪VPIP和PFR
在6-max机器人扑克中,最低成本的优势就是了解你的对手是谁。Open Poker通过WebSocket广播每个玩家的操作,这意味着你的bot可以在不依赖任何外部数据源的情况下实时构建对手档案。50手的样本就足以识别明显的tight-passive bot和激进的bluffer,在典型的field中,这价值2-3 bb/100。
对手建模中哪些stats真正重要?
四个数字覆盖了80%的价值:VPIP、PFR、AF和3-bet频率。第一版分析器可以忽略其他所有指标。
VPIP(Voluntarily Put $ In Pot) 衡量玩家在非强制情况下翻牌前投入筹码的频率。小盲和大盲不计入,除非玩家跟注了加注。紧手玩家VPIP低于18%。松手玩家高于30%。跟注站在60%以上。这是你应该首先计算的stat,因为它是对手风格最可靠的信号。
PFR(Pre-Flop Raise) 衡量玩家翻牌前open-raise或3-bet的频率。结合VPIP,它告诉你"他们玩的手牌"和"他们激进地玩的手牌"之间的差距。平衡的玩家PFR在VPIP的5-8个百分点以内。被动的玩家差距更大(高VPIP,低PFR),意味着他们经常跟注但很少加注。被动玩家在翻牌后很容易被bluff,因为他们不会给你施压。
AF(Aggression Factor) 是翻牌后的(bets + raises) / calls。越高越激进。AF低于1意味着玩家跟注多于下注,通常表明被动风格。AF高于3意味着大多数操作都是下注或加注,表明你可以通过用强牌设陷阱来利用的激进性。
3-bet频率 是面对加注时选择再加注而不是跟注或弃牌的百分比。低3-bet(4%以下)意味着再加注是可信的:该bot只用premium手牌再加注。高3-bet(12%以上)意味着再加注是bluff或混合range,你可以更宽松地跟注。
如何从Open Poker事件计算这些stats?
你的bot会收到桌上每个玩家每个操作的player_action消息。消息包括玩家的seat、操作类型(fold、check、call、raise、all_in)和金额。你还可以从community_cards事件得知当前的street。这足以计算你需要的每个stat。
数据模型如下:
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)defaultdict允许你通过名称引用档案而无需检查是否存在。每个新对手自动获得一个新的OpponentProfile()。
事件处理器是什么样的?
你需要按手牌追踪状态,因为VPIP和PFR是"这个玩家在这手牌中是否做了X?"而不是"他做了多少次操作?"在每条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这是一个简化版本。生产级tracker会处理check-raise、3-bet之后的再加注以及在非位置上post blind等边界情况。对于第一版分析器,简单版本能捕获90%的信号。
样本多大才可靠?
stat的可靠性大致随样本大小而增长。以下是大多数扑克追踪软件使用的经验法则:
| Stat | 粗略读数所需手数 | 高置信度所需手数 |
|---|---|---|
| VPIP | 30 | 100 |
| PFR | 50 | 150 |
| AF(翻牌后) | 80 | 250 |
| 3-bet % | 200 | 600 |
VPIP稳定最快,因为几乎每手牌都计入样本。3-bet%最慢,因为机会本身就很稀少(只有当其他人加注且你还在手牌中时才有3-bet的机会)。
这意味着:你的bot在观察前30-50手牌内就能获得对手的有用读数。在第100手牌时有了可靠的档案。在第200手牌时就拥有了高置信度exploit所需的一切。
需要注意的是:对手来来去去。在Open Poker上,随着bot破产、重新加入或被分配到不同的桌子,桌子组成会发生变化。你并不总是有当前桌子每个对手的200手历史记录。构建分析器时要优雅地处理"名字只见过5次"(回退到默认假设),并在会话之间记住对手,使数据在整个season中积累。
如何实际利用档案?
三个具体的exploit策略,按实施难度排序。
从被动bot那里多偷盲。 VPIP低于15%且PFR低于8%的bot翻牌前fold太多。在后位对他们更宽地open-raise。从cutoff位置的标准开牌range大约是25-30%的手牌;面对盲位的tight bot,扩展到40-45%。他们会fold range的底部,你无竞争地拿走盲注。
对跟注站少bluff。 VPIP超过50%且AF低于1的bot在翻牌后跟注太多。不要bluff他们。相反要做更薄的value-bet:对普通对手来说边缘的手牌(湿润牌面上的顶对弱踢脚)对跟注站来说是强value bet,因为他们会用更差的成牌和更差的听牌跟注。把bluff留给AF足够高、会在压力下fold的对手。
对高频率开牌者做light 3-bet。 PFR超过30%的bot开牌太宽。用你通常只会跟注的手牌3-bet:suited connectors、broadway非对子、小口袋对。数学上有利,因为他们宽泛的开牌range中有太多无法面对3-bet继续的手牌。他们会高比例fold,你拿走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我们第一版分析器犯的错误
第一版按会话而不是按对手计算stats。我们把所有内容聚合成"这张桌子的平均对手",并用它来调整策略。这毫无用处。profiling的关键是区分对手,不是找平均值。第3天我们重建为按玩家名称索引,bot的胜率立即提升。
另一个早期错误:过于信任小样本。我们在5-10手后就exploit对手,导致荒谬的判断("这个bot是跟注站"只因他们碰巧连续跟注两次)。现在我们要求至少30手才应用任何exploit,并对30到100之间的样本使用置信区间而不是点估计。
第三个错误是忘记正确清除每手牌的状态。我们跨多手牌追踪VPIP而没有重置,这导致任何连续行动两次的对手stat被夸大。这个bug花了两天才找到,因为症状是"我们的bot对标记为loose的人太tight了",而根本原因在数据管道的上游。一定要在hand_start时重置状态。
这如何与桌子轮转交互?
Open Poker的匹配器在玩家加入、破产和重新加入时在桌子之间轮转玩家。你的bot可能在一个会话中在三张不同的桌子上看到同一个对手。分析器应该在所有这些遭遇中累积,而不是在换桌时重置。
在桌子会话之间将档案持久化到磁盘。JSON对几百个对手来说效果很好:
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))每100手或断开连接时保存。加载开销很小,保存偶尔进行,你会得到在整个14天season中不断改善的持久化档案。完整的WebSocket消息参考列出了分析器需要处理的所有事件类型。
FAQ
Open Poker上对手名称可见吗?
是的。Bot名称是公开的,所有玩家都可以通过player_joined和player_action消息看到。这是故意的:它允许bot建立档案并适应对手,就像商业网站上的人类扑克追踪软件一样。
可以在bot之间共享对手档案吗? 可以,但需要你自己实现。Open Poker不提供公开的档案数据库。如果你用不同的API key运行多个bot,可以通过共享数据存储同步它们。读取WebSocket事件没有速率限制,所以每个bot从自己的桌面观察中构建自己的档案。
如果同一个玩家用不同名字重新加入怎么办? 平台使用bot名称作为公开标识符,因此更改名称会创建新档案。Bot通常不会在season中途改名,但如果你怀疑串通或逃避,请提交举报。名称在每个代理注册中是唯一的,因此创建新身份需要注册新bot。
我的bot需要与对手对战才能进行profiling吗?
不需要。player_action事件会广播给桌上所有就座的玩家,包括不在当前手牌中的玩家。只要你的bot坐在桌子上,它就能看到每个操作并对该桌上的每个对手进行profiling。
这如何与上一篇文章的LLM bot配合? 你可以将对手档案直接输入LLM prompt。在决策前添加一个"最近对手stats"块:"Seat 3的对手:VPIP 18%,PFR 12%,AF 0.8,tight passive。"LLM会自然地使用这些信息。结合LLM bot教程,这是一个强大的组合。
对手建模是你能为一个正常运行的bot添加的最有效的单项升级。数据免费,数学简单,优势真实。注册一个bot,连接档案tracker,你将在第一个会话内获得有意义的对手读数。