高级扑克机器人架构:胜率与测试
高级扑克机器人把可靠的事件循环变成可衡量的决策系统。升级并不等于“加入 AI”,而是引入不可变决策快照、明确的扑克特征、考虑范围的胜率、可回放牌局,以及能够证明策略发生变化的实验循环。保留基础机器人的合法动作防护层,所有更智能的能力都放在它后面。
扑克机器人架构系列: 第 2 篇,共 3 篇。如果运行时仍会产生被拒绝的动作,请先阅读基础扑克机器人架构。接着阅读专业扑克机器人架构。可在2026 年扑克机器人成本中比较运维方案。
高级扑克机器人架构有哪些变化?
高级扑克机器人会分离事实、估计、策略和执行。事实来自服务器最新回合,包括底池、公共牌、筹码量、合法动作、牌局 ID 和令牌。估计包括胜率和对手范围。策略使用两者返回意图。执行层最后一次对照事实检查该意图。
这种区分很重要,因为估计可以出错。对手模型可能给出过紧的范围,蒙特卡洛运行可能存在采样误差。任何一种错误都不应产生非法加注或对已过期回合的响应。即使内部策略变得概率化,外层仍保持确定性。
| 数据类别 | 示例 | 可信度 | 负责人 |
|---|---|---|---|
| 服务器事实 | pot = 180,加注上限 1,640 | 权威 | 协议适配器 |
| 派生事实 | 底池赔率、有效筹码、位置 | 输入正确时精确 | 特征层 |
| 估计 | 摊牌胜率 43% | 采样或建模所得 | 胜率服务 |
| 判断 | 对手在按钮位以 31% 的范围开池 | 不确定 | 对手模型 |
| 意图 | 胜率超过价格,因此跟注 | 策略输出 | 策略层 |
绝不要把这些内容压平到一个无类型字典里。可信度和归属一旦消失,猜测出的范围就会看起来和服务器的加注边界同样可靠。
不可变决策快照应如何工作?
不可变决策快照是从一次 your_turn 捕获的完整只读输入。它能防止胜率计算或模型运行期间,迟到的网络事件改变数据。它也会成为测试中的回放单位。
from dataclasses import dataclass
from typing import Literal
Action = Literal["fold", "check", "call", "raise", "all_in"]
@dataclass(frozen=True)
class DecisionSnapshot:
hand_id: str
turn_token: str
street: str
hole: tuple[str, str]
board: tuple[str, ...]
pot: float
stack: float
to_call: float
opponents: int
position: str
legal: tuple[Action, ...]
min_raise: float | None
max_raise: float | None
@property
def pot_odds(self) -> float:
return self.to_call / (self.pot + self.to_call) if self.to_call else 0.0
@property
def stack_to_pot(self) -> float:
return self.stack / self.pot if self.pot else float("inf")回合到达时同步构建快照,然后将其传给异步决策任务。发送结果前,再次将结果的 hand_id 和 turn_token 与当前回合比较。Open Poker 令牌会在执行动作后被消耗,每个新的 your_turn 都会使前一个失效,因此必须丢弃过期工作,而不是重试。
消息类型参考记录了线上字段。标准化器还应在重连后接受 table_state,因为其玩家专属的 hero 部分包含底牌和当前合法动作。
如何用胜率和底池赔率做出决策?
胜率回答的是一手牌对抗假定范围时在摊牌中获得底池的频率,底池赔率回答的是跟注所需的盈亏平衡份额。在考虑未来下注、范围误差和锦标赛目标之前,当估计胜率高于 call / (pot + call) 时,跟注具有正筹码期望。
如果底池为 180,跟注需要 60,底池赔率就是 60 / 240 = 25%。38% 的胜率估计比这个原始阈值高 13 个百分点。不要在只有一个百分点优势的每个场景中都跟注。蒙特卡洛方差、不准确的对手范围和后续行动都可能抹去微弱优势。我们使用明确的安全余量并记录它。
from dataclasses import dataclass
@dataclass(frozen=True)
class EquityResult:
equity: float
trials: int
range_name: str
def call_has_margin(snapshot: DecisionSnapshot, result: EquityResult,
margin: float = 0.03) -> bool:
return (
"call" in snapshot.legal
and result.equity >= snapshot.pot_odds + margin
)蒙特卡洛胜率计算器展示了 Python 实现。对于持续维护的评估基础组件,PokerKit 的牌型评估器文档比在策略项目中自行编写牌型比较器更适合作为起点。
对手模型应如何表示不确定性?
对手模型应保存计数和平滑后的比率,而不是“激进”或“鱼”之类的标签。实用的早期特征包括主动翻牌前入池率、翻牌前加注率、三下注机会和动作、翻牌后激进度、面对下注的弃牌机会,以及观察到的摊牌牌型。每个比率都必须有分母。
一名玩家在四次机会中加注两次,观察比率为 50%,但几乎没有确定性。贝叶斯平滑可以避免小样本让策略剧烈摆动。使用 Beta 先验时,可将比率估计为 (successes + alpha) / (opportunities + alpha + beta)。中性的 Beta(2, 2) 先验会把四次机会中的两次加注变为 4 / 8 = 50%,而一次机会中零次加注会变为 2 / 5 = 40%,而不是武断地判定为零。
应按会改变策略的情境细分统计数据。位置和下注轮次都很重要。牌桌级别的“激进度”会把枪口位开池和河牌过牌加注混为一谈。先从少量能积累足够样本的分桶开始,而不是创建五十个稀疏单元。我们的对手建模指南更深入地介绍了衰减、摊牌证据和剥削上限。
保留一个忽略对手特征的基线策略。没有它,你无法判断自适应是否有效,还是整个机器人只是在一周内运气不错。
高级策略接口应返回什么?
高级策略应返回动作意图、金额意图、原因代码、特征值和模型元数据。像 "call" 这样的普通字符串不足以用于分析,而自由文本长文对代码来说又过于松散。请使用类型化记录。
from dataclasses import dataclass, field
@dataclass(frozen=True)
class DecisionIntent:
action: Action
amount: float | None
reason_code: str
confidence: float
features: dict[str, float] = field(default_factory=dict)
policy_version: str = "range-equity-v3"
def decide(snapshot: DecisionSnapshot, eq: EquityResult) -> DecisionIntent:
features = {
"equity": eq.equity,
"pot_odds": snapshot.pot_odds,
"spr": snapshot.stack_to_pot,
}
if snapshot.to_call == 0 and "check" in snapshot.legal:
return DecisionIntent("check", None, "free_action", 1.0, features)
if call_has_margin(snapshot, eq):
confidence = min(1.0, (eq.equity - snapshot.pot_odds) * 4)
return DecisionIntent("call", None, "equity_clears_price", confidence, features)
return DecisionIntent("fold", None, "equity_below_price", 0.8, features)边界约束和后备动作仍由执行防护层负责。如果策略提出跟注时当前回合已经改变,就丢弃结果。如果策略提出不在合法列表中的加注,就替换为过牌或弃牌,并增加策略违规指标。静默修正会隐藏错误,修正并记录遥测则既能保护牌桌,也能让故障可见。
牌局回放如何测试完整决策路径?
牌局回放无需打开套接字,而是让录制的服务器事件经过线上使用的同一套标准化器和策略。它能捕获独立单元测试遗漏的错误,例如跨牌局泄漏的状态、从空座位推导的位置、重复事件,或进入算术运算的 null 金额。
每行保存一条 JSON 消息,并保持到达顺序。回放运行器可以加载文件并调用真实处理器。用固定种子替换随机性,用录制的响应替换外部服务。输出应是一系列决策记录,测试可以将其与获批行为进行比较。
import json
import random
from pathlib import Path
def replay(path: str, engine) -> list[dict]:
random.seed(20260812)
decisions = []
for line in Path(path).read_text(encoding="utf-8").splitlines():
event = json.loads(line)
result = engine.apply(event)
if result is not None:
decisions.append(result)
return decisions
def test_replay_never_emits_illegal_action(engine):
for item in replay("fixtures/three_hands.jsonl", engine):
assert item["action"] in item["legal"]基于属性的测试很适合动作防护层。生成随机合法动作集合和加注边界,然后断言输出始终在服务器提供的动作中,且始终位于边界内。Hypothesis正是为这类测试构建的。并非每个扑克观点都需要属性测试,但协议不变量值得这样验证。
如何评估策略变更?
评估策略变更时,应使用成对策略、固定版本标签、运维指标,以及足以体现不确定性的手数。不要部署新范围后,把接下来的 200 手牌与上周二的结果比较。对手组成、位置和发牌方差都已经不同。
至少跟踪以下指标:
| 指标 | 重要性 |
|---|---|
| 每 1,000 个回合的非法动作数 | 硬性正确性门槛 |
| 决策延迟 p50、p95、p99 | 检测被平均值掩盖的长尾故障 |
| 后备策略使用率 | 显示策略或依赖项的不稳定性 |
| 含置信区间的 bb/100 | 按大盲标准化的策略结果 |
| 全下调整后估计 | 减少部分摊牌方差 |
| VPIP、PFR、三下注率 | 解释行为如何变化 |
| 各下注轮次弃牌率 | 发现明显的策略漏洞 |
条件允许时,在模拟器中使用共同随机数,让策略 A 和 B 面对相同的发牌和对手动作。实时对局无法完全控制环境,因此应记录牌桌构成并比较更长窗口。Google DeepMind 的 OpenSpiel 论文介绍了适用于游戏的评估与研究框架,其中包括不完全信息游戏。它适用于本地研究,而实时 Open Poker 竞技场则用于测试协议和对手多样性。
不能仅因为 bb/100 为正就推广一项变更。它必须保持正确性门槛为零、延迟在预算内,并改善预先声明的目标,同时不在其他方面造成不可接受的退化。
LLM 在高级阶段应放在哪里?
LLM 应作为选择性顾问,放在与其他策略相同的接口后面,而不是充当网络客户端或合法性权威。向它提供紧凑快照、精确的合法动作、计算出的底池赔率、估计范围和严格 JSON 模式。验证其答案,并保留确定性的后备策略。
显而易见的决策应绕过模型。免费过牌、严格翻牌前表格要求的弃牌,或已由确定性价值规则选出的加注大小,都不需要远程调用。选择性路由可以降低成本,也让延迟更容易控制。LLM 扑克机器人指南展示了基本连接模式,但高级运行时还应加入模式验证、版本化提示词、截止时间取消和回放固定样例。
我们不把自然语言理由视为可靠的评估证据。模型可以为糟糕动作生成很有说服力的解释。应根据结果、反事实测试和稳定指标判断动作。文本可用于调试,但应信任结构化特征和实验记录。
常见问题
什么让扑克机器人成为高级机器人?
高级机器人使用不可变回合快照、明确的派生特征、考虑范围的胜率、对手统计、回放测试和版本化实验。单纯增加策略代码并不会让架构变得高级。
扑克机器人应该运行多少次蒙特卡洛试验?
从每次决策 2,000 到 5,000 次试验开始,并在你的硬件上进行基准测试。只有当增加试验次数足以改变决策并值得付出延迟时,才进一步增加。试验次数和运行时间都要记录。
对手建模需要多少数据?
从第一次观察起就使用平滑,但在每项统计拥有有意义的分母之前,应让自适应幅度保持较小。没有一个适用于所有情况的神奇手数,因为三下注机会远少于翻牌前入池机会。
我应该优化胜率还是筹码利润?
使用每 100 手牌的大盲数来生成可比较的策略报告,并用原始筹码衡量赛季影响。始终公布不确定性和运维指标。缺少手数和区间的点估计很容易制造虚假信心。
回放测试能证明扑克策略会赢钱吗?
不能。回放能证明确定性行为,并捕获已知情形中的回归。模拟和实时实验用于测试策略面对不同牌局和对手分布时的表现。
当策略版本、牌局回放和指标让每次变更都可审计时,下一个约束就是运维。专业扑克机器人架构介绍重新同步、截止时间预算、故障隔离、影子评估,以及让机器人无人值守运行的安全发布方法。