Skip to content
[OPEN_POKER]

用 Python 构建蒙特卡罗扑克胜率计算器

JJoão Carvalho||已更新 |13 min read

蒙特卡罗扑克胜率计算器,是最先能真正改善决策的数学升级。机器人不再靠“我有听牌,也许能跟注”这种昂贵猜测,而是估算获胜概率,再与底池赔率比较。

本篇属于2026 年 AI 扑克机器人完整构建指南,该核心指南涵盖框架、决策逻辑、胜率、测试与参赛平台。

核心要点

  • 蒙特卡罗模拟通过抽样未知对手底牌与后续公共牌来估算胜率。
  • Open Poker 会在 your_turn 中提供准确的底池与跟注金额,因此胜率可直接用于底池赔率决策。
  • 每次决策可从 2,000 至 5,000 次试验起步。试验越多结果越稳定,但也要控制行动延迟。

蒙特卡罗胜率解决什么问题?

蒙特卡罗胜率弥补了当前牌型标签与未来发牌结果之间的差距。在 Open Poker 中,机器人有 120 秒行动时间,并在每条 your_turn 消息中收到底池、公共牌、合法动作与回合令牌(Open Poker 动作文档)。这些信息足以在发送动作前判断跟注是否有利可图。

计算器要回答的是:如果把未知牌重复发许多次,我的牌获胜或平分底池的比例是多少?这个比例就是当前胜率。

如果底池为 300,跟注为 100,则底池赔率为:

100 / (300 + 100) = 25%

如果您的抽样赢率是 36%,那么在未来下注影响之前跟注是有利可图的。如果你的胜率是 18%,弃牌可以节省筹码。这个数字并不完美,但它比硬编码的“跟注任何同花听牌”要好得多。

您需要从 Open Poker 获得哪些信息?

你需要四个值:己方底牌、公共牌、底池大小和跟注金额。Open Poker 会在 your_turn 中发送公共牌与底池;机器人则应缓存私有 hole_cards 消息中的底牌。WebSocket 协议基于 JSON,任何语言都能接入(WebSocket 文档)。

def call_amount(valid_actions):
    for action in valid_actions:
        if action["action"] == "call":
            return action["amount"]
    return 0
 
 
def pot_odds(pot, call):
    if call <= 0:
        return 0
    return call / (pot + call)

既然服务器已经提供底池,就不要自行估算。机器人若根据早期 player_action 事件重建状态,很容易漏掉重连、被拒动作或边池细节,引发难以追踪的错误。让服务器负责权威状态,让机器人专注决策。

如何比较最终牌力?

使用 PokerKit 的 StandardHighHand.from_game() 比较最终德州扑克牌力。PokerKit 支持标准高牌规则与常规比较运算符,示例见其牌力评估文档

from pokerkit import StandardHighHand
 
RANKS = "23456789TJQKA"
SUITS = "cdhs"
DECK = [r + s for r in RANKS for s in SUITS]
 
 
def compact(cards):
    return "".join(cards)
 
 
def best_hand(hole, board):
    return StandardHighHand.from_game(compact(hole), compact(board))

这个小封装能保持计算器其余部分清晰易读。Open Poker 已使用 AsTd7c 这类格式表示牌,因此除非机器人采用不同存储格式,否则无需自定义转换器。

如果性能成为问题,请缓存解析的卡。 PokerKit 的文档警告说,重复的字符串解析代价高昂。对于第一个机器人,直接字符串就可以了。对于在多个托管机器人中每个操作运行 10,000 次试验的机器人来说,分析变得值得。

你如何估计对一个对手的胜率?

抽样尚未发出的牌与对手底牌,再比较最终牌力。运行 5,000 次试验后,简单的 Python 计算器通常已足够稳定,可用于 Open Poker 实时决策。PokerKit 的基准测试显示,在笔记本电脑 CPU 上每秒可完成超过一百万次标准高牌制牌力评估,因此评估器通常不会最先成为瓶颈(PokerKit 文档)。

from random import sample
 
 
def estimate_equity(hero, board=None, opponents=1, trials=5000):
    board = board or []
    known = set(hero + board)
    missing_board = 5 - len(board)
    equity = 0.0
 
    for _ in range(trials):
        deck = [card for card in DECK if card not in known]
        draw_count = opponents * 2 + missing_board
        draw = sample(deck, draw_count)
 
        opp_holes = [
            draw[i * 2 : i * 2 + 2]
            for i in range(opponents)
        ]
        runout = board + draw[opponents * 2 :]
 
        hero_hand = best_hand(hero, runout)
        all_hands = [hero_hand] + [
            best_hand(opp, runout)
            for opp in opp_holes
        ]
 
        winning_hand = max(all_hands)
        winners = [hand for hand in all_hands if hand == winning_hand]
 
        if hero_hand == winning_hand:
            equity += 1 / len(winners)
 
    return equity / trials

这个函数会正确计算平分底池时的份额。若你与一名对手并列获胜,胜率计入二分之一;三方并列则计入三分之一。在成对公共牌面或顺子公共牌面上,这种处理尤其重要。

如何把胜率转化为行动?

把胜率与底池赔率比较,再留出一小段安全余量。Open Poker 采用 6 人桌、固定 10/20 盲注,买入范围为 1,000 至 5,000 筹码(赛季文档),一次错误跟注仍可能损失相当可观的筹码。

def equity_decision(msg, hole_cards, opponents=1):
    actions = {a["action"]: a for a in msg["valid_actions"]}
 
    if "check" in actions:
        return {"action": "check"}
 
    if "call" not in actions:
        return {"action": "fold"}
 
    call = actions["call"]["amount"]
    pot = msg["pot"]
    board = msg.get("community_cards", [])
 
    equity = estimate_equity(
        hero=hole_cards,
        board=board,
        opponents=opponents,
        trials=3000,
    )
 
    required = pot_odds(pot, call)
    margin = 0.03
 
    if equity > required + margin:
        return {"action": "call"}
    return {"action": "fold"}

该政策仍然是被动的。它会跟注有利可图的位置,但不会加注价值牌或半诈唬强听牌。一旦计算器工作,将其与扑克机器人投注策略配对。

实时机器人应该运行多少次试验?

翻牌圈和转牌圈可从 3,000 次试验起步;若延迟成为问题,再降低次数。Open Poker 允许每个连接每秒发送 20 条 WebSocket 消息,每次行动时限为 120 秒,但机器人仍应尽快行动。缓慢计算若阻塞事件循环,会让机器人错失更多机会。

不同下注轮使用不同试验次数:

下注轮建议试验次数原因
翻牌前1,000足以进行粗略范围检查
翻牌5,000还剩下两张牌,差异很大
3,000还剩一张卡,预估稳定更快
0无需模拟,评估成手牌

对抗随机牌的翻牌前赢率本身是不够的。您还需要位置范围,因为对手不会随机出牌。使用 扑克机器人位置范围 缩小机器人在翻牌前打开的手牌范围。

有哪些陷阱?

最大的陷阱,是把“当前胜率”误当成“最终利润”。胜率没有计入后续下注。同花听牌在翻牌圈可能有 35% 胜率,但若转牌未击中且对手持续施压,你可能必须弃牌,根本无法实现这部分胜率。

第二个陷阱是对手数量。随机一手牌的赢率并不等于五名活跃玩家的赢率。多路底池会压垮弱听牌,因为其他人更有可能提高。当牌局中还有更多玩家时,增加“对手”参数。

第三个陷阱是桌子纹理。如果你的机器人面对的是非常松散的对手,那么随机手牌的假设会更接近。对抗紧手的机器人,他们的跟注范围比随机的强,而且你的胜率估计过于乐观。这就是对手建模开始收回成本的地方。

如何让计算器保持快速运行?

河牌阶段少做无用计算,才能维持速度。实时机器人不需要为每次边缘跟注追求实验室级精度,只需足够快、足够稳定地给出正确答案,并避免阻塞 WebSocket 循环。这意味着使用缓存、按下注轮调整试验次数,并尽早退出。

使用三个简单的规则:

优化为什么它有帮助
跳过河牌模拟五张公共牌均已知
在小盆中进行较低的试验20 个芯片的错误不值得 10,000 次尝试
缓存重复输入重试或重新同步时可能会出现相同的孔和板
equity_cache = {}
 
 
def cached_equity(hero, board, opponents, trials):
    key = (tuple(sorted(hero)), tuple(board), opponents, trials)
    if key not in equity_cache:
        equity_cache[key] = estimate_equity(hero, board, opponents, trials)
    return equity_cache[key]

缓存不需要永远存在。在双手之间清除它或将其限制在几百个条目。目标是避免意外的重复工作,而不是建立每个可能的德州扑克状态的数据库。

如果机器人使用 LLM 决策,请先计算胜率,再把百分比传入提示词。明确的数字能减少 LLM 扑克决策中的模糊空间。

常见问题解答

蒙特卡罗权益准确吗?

不。这是抽样估计值。许多点都可以进行精确枚举,但蒙特卡洛更容易连接到实时机器人,并且速度足够快,适合 Open Poker 的 120 秒操作超时。

我的机器人应该在翻牌前运行赢率吗?

只能作为辅助信号。翻牌前决策应从位置范围开始,因为按钮位的 A9o 与枪口位的 A9o 处境完全不同。对随机底牌计算的胜率忽略了这一背景。

我可以在没有 PokerKit 的情况下使用它吗?

是的,但是你需要另一个手牌评估器。 PokerKit 可以节省时间,因为它直接支持 Hold'em 手牌构建和比较。编写自己的评估器是一个有趣的项目,也是一个糟糕的第一个生产决策。

我如何测试这是否可以改善我的机器人?

运行两个版本至少几百手:一种使用旧的跟注逻辑,另一种使用权益门控的跟注。通过公开扑克赛季统计数据和排行榜跟踪净筹码、玩过的牌局和失败的场次。

继续阅读