☰
炸金花AI决策:基于EV期望收益的Python实现与策略解析
2026/10/5 1:05:36 网站建设 项目流程

炸金花游戏做到第三篇,最关键的东西总算登场了:EV。前两篇我们把规则、牌型强度、胜率模拟跑通,但那还属于静态评估;这一篇要解决真正的问题——AI拿到一手牌,面对底池和对手的下注,到底该跟、该加、还是该弃。答案不在感觉里,在数字里。我打算用一个基于EV(期望收益)的简单AI模型,把每一步决策都拆成可计算的账,并直接给出Python实现和完整决策流程。不管你是想研究棋牌类AI、练手策略算法,还是纯粹想写个能陪你打牌的机器人,这篇都可以直接用。

1. 为什么AI要盯住EV:从“猜牌”到“算账”

1.1 赌徒思维 vs 程序员思维

很多玩家打炸金花,靠的是“这把我感觉能行”“他刚才这么下注肯定在偷鸡”,这种直觉型判断在短局里偶尔很灵,但拉长到几百手、几千手,最后赢钱的往往是数学上站得住脚的那一方。

程序员做AI,要做的就是把这套“感觉”替换成一套可复用的数字框架。牌局里有大量随机因素:对手手牌未知、剩余牌堆未知、对手的心理和习惯未知。面对不确定性,AI最稳的决策依据不是“猜对方有没有大牌”,而是“我这个动作长期重复做,平均能赚多少、亏多少”。这个平均收益就是EV(Expected Value,期望收益)。

EV并不是一个能保证单局胜利的魔法数字,它是一个统计量。你可以把它理解成一个投资的“长期年化收益率”:某一手牌可能赢,也可能输,但只要每次都在正EV的时机出手,长期累积下来,利润就会向数学期望靠拢。反过来,经常做负EV的决策,哪怕偶尔赢几把大的,最终也会被统计规律拉回去。

1.2 EV核心公式与一次下注的拆解

EV的核心公式并不复杂:

EV = 胜率 × 赢到的钱 - 败率 × 本次投入的钱

用符号写就是:

EV = w × P - (1 - w) × C

其中:

  • w 是你估算的胜率;
  • P 是当前底池里已有的筹码量;
  • C 是你需要跟注的筹码量;
  • 1 - w 是失败概率。

举个例子,底池100,对手下注20,你需要跟注20才能继续。假设你通过模拟估算出胜率是30%,那么:

EV = 0.3 × 100 - 0.7 × 20 = 30 - 14 = 16

EV是正数,说明这个跟注动作长期看是赚钱的。哪怕你这把输了20,只要类似局面重复100次,每次赢的时候平均能赢回100底池中的相应份额,整体依然有正收益。

还有一种更精细的写法,把你自己跟注进去的20也算进赢的时候能拿回来的部分:

EV = w × (P + C) - C

因为当你赢下底池时,你之前投入的C也包含在底池里会一起回来。代入上面的数字,结果其实是相同的:

EV = 0.3 × 120 - 20 = 36 - 20 = 16

两种写法本质一致,关键是定义清楚“P”是跟注前的底池还是跟注后的底池。代码实现时统一口径,别混用。

1.3 为什么EV模型能成为AI的决策依据

只要给AI定义好所有可选动作的EV,AI就可以用一条非常简单的规则做决策:选择EV最大的动作。

  • 如果弃牌的EV是0,跟注的EV是16,那就跟注;
  • 如果加注到50能算出EV是22,那就比跟注更优;
  • 如果所有动作的EV都小于0,那就弃牌。

这个规则看起来简单,但它背后的逻辑很深:在信息不完整的博弈中,只要你的胜率估算足够准、对手行为预测足够合理,最大化期望收益就是接近最优的策略。德州扑克里的GTO策略,本质上也是在追求一种“不被剥削”的EV平衡;炸金花虽然规则不同,但EV这个分析工具完全通用。

当然,EV模型的精度完全取决于输入:胜率估得准不准、对手弃牌率估得准不准。这也是为什么后面会花大量篇幅讲模拟和对手建模,这两件事决定了EV模型到底是“印钞机”还是“计算器玩具”。

2. 炸金花AI需要喂哪些信息

2.1 手牌强度:胜率怎么来

EV公式里的w是胜率,但胜率不是固定的“手牌等级表”。一手牌的胜率取决于你有几个对手、对手的手牌范围、剩余牌堆的随机性。比如一对A在单挑时胜率很高,但在5人局里会因为更多人抽到更大牌而下降不少。

计算胜率有两种常用方法,我分别说下。

第一种是枚举法。在炸金花里,如果只有你和一名对手,且你已看牌,那么对手的牌是从剩余牌堆里随机组合的。理论上你可以把所有组合都遍历一遍,得到精确胜率。

第二种是蒙特卡洛模拟。随机给对手发几万次手牌,每次比较牌型大小,赢的次数除以总次数就是胜率。这个方法更通用,适合多人局,速度也够快。

我在实际项目里优先用蒙特卡洛,因为它的实现简单、错误率可控,而且炸金花的牌型判断本身不复杂,几万次模拟在现代CPU上也就几十毫秒。

下面是一段简化版的手牌评估和胜率模拟代码:

import random from itertools import combinations RANKS = '23456789TJQKA' SUITS = 'SHDC' def card_value(card): rank, suit = card[0], card[1] return RANKS.index(rank) + 2 def evaluate(hand): """ 返回牌型等级和比较用的排序值,数值越大牌越大 等级:豹子>同花顺>同花>顺子>对子>散牌 """ values = sorted([card_value(c) for c in hand], reverse=True) suits = [c[1] for c in hand] unique = set(values) is_flush = len(set(suits)) == 1 is_straight = max(values) - min(values) == 2 and len(unique) == 3 if len(unique) == 1: return (6, values) # 豹子 if is_flush and is_straight: return (5, values) # 同花顺 if is_flush: return (4, values) # 同花 if is_straight: return (3, values) # 顺子 if len(unique) == 2: return (2, values) # 对子 return (1, values) # 散牌 def monte_carlo_win_rate(my_hand, num_opponents=1, iterations=10000): deck = [r + s for r in RANKS for s in SUITS] for c in my_hand: deck.remove(c) win = 0 for _ in range(iterations): random.shuffle(deck) # 给每个对手发3张牌 opponents = [ evaluate(deck[i * 3: i * 3 + 3]) for i in range(num_opponents) ] my_score = evaluate(my_hand) if my_score > max(opponents): win += 1 return win / iterations

代码里几个细节要注意:

  • 枚举对手手牌时,必须先把已知手牌从牌堆中移除,否则会出现“自己和对手拿到同一张牌”的荒谬结果。
  • 牌型等级的比较直接放在元组里,Python的元组比较是逐位比较的,后面再接一个values,是为了解决同等级牌型下谁更大的问题。
  • 如果你的本地规则包含A23这类的特殊顺子,需要在判断顺子时单独处理,很多人会在这里踩坑。

2.2 行动空间与输入特征

炸金花相比德州的一个特色是“闷牌”,也就是玩家可以选择不看自己的牌直接下注。简单EV模型不考虑这个复杂情况,先假设AI已经看牌,只看牌后怎么决策。对于未看牌的阶段,完全可以沿用固定策略,比如“前两轮默认跟注,后两轮随机决定看牌或弃牌”,这样也能跑。

一份完整的回合内输入信息包括:

  • 当前手牌;
  • 当前底池大小;
  • 当前轮到AI时要跟注的金额;
  • 可选的加注金额;
  • 对手数量;
  • 对手历史行为数据(用于估算弃牌率)。

AI的动作空间包含弃牌、跟注、加注和比牌。比牌在炸金花里是单独支付一定费用,直接跟你指定的一名对手比大小,输的人支付底池对应的筹码。从EV角度看,比牌的本质是“用一笔固定费用,换取一次以胜率w赢下底池的机会”,因此它的EV计算和跟注很像,只是费用结构不同。

2.3 EV能覆盖所有行动吗

跟注的EV可以直接用前面的公式,但加注的EV要复杂一些,因为对手可能会弃牌,也可能会再反加。一个简单但好用的处理方式是引入“对手弃牌率fold_rate”:

  • 如果对手弃牌,你直接赢下当前底池P,收益是P;
  • 如果对手不弃牌,你和对手进入下一轮或者直接开牌,此时你的收益按胜率w结算:
    • 赢了拿回底池加注后的筹码,输了失去加注额;
    • 简化公式:EV = fold_rate × P + (1 - fold_rate) × [w × (P + R) - R]

这里的R是你加注的金额。下面我会用这个公式实现AI。

3. 用Python实现EV计算核心模块

3.1 两个EV函数:跟注和加注

结合上面的分析,我先写两个基础函数:

def ev_call(win_rate, pot, call_cost): """ 跟注的期望收益 pot:跟注前的底池 call_cost:需要跟注的金额 """ return win_rate * (pot + call_cost) - call_cost def ev_raise(win_rate, pot, raise_to, fold_rate=0.2): """ 加注到raise_to的期望收益 简化模型:对手只有 弃牌/跟注 两种反应,不考虑反加 """ win_if_called = win_rate * (pot + raise_to) - raise_to return fold_rate * pot + (1 - fold_rate) * win_if_called

两个函数都假设加注后能直接摊牌,也就是“对手跟注后不会再施加额外压力”,这在实际游戏里并不总是成立,但作为简单模型已经够先跑起来。

3.2 决策逻辑:最大化EV

决策模块要做的事是:遍历所有可选动作,计算每个动作的EV,选最大的那个。

def decide_action(win_rate, pot, to_call, min_raise=10): actions = [] # 动作1:弃牌,EV恒为0 actions.append(('fold', 0, 0)) # 动作2:跟注 if to_call > 0: actions.append(('call', to_call, ev_call(win_rate, pot, to_call))) else: actions.append(('check', 0, 0)) # 不需要跟注时免费过牌 # 动作3:加注到不同额度 for r in set([min_raise, min_raise * 2, min_raise * 5]): if r > to_call: actions.append(('raise', r, ev_raise(win_rate, pot, r, fold_rate=0.25))) # 从所有动作里挑EV最大的 best = max(actions, key=lambda x: x[-1]) return best

这里需要注意,加注到不同额度EV计算里的fold_rate,我固定取0.25,实际中应该根据对手历史动态调整。加注额度与对手弃牌率通常是正相关的:你加注越多,对手越倾向弃牌,但同时一旦被跟注,你输得也越多。这两个效应之间存在一个平衡点,后面会讲怎么调。

3.3 完整AI模块

把胜率估算和EV决策串起来,做一个简单的AI类:

random.seed(42) class SimpleEVBot: def __init__(self, iterations=10000, fold_rate=0.2): self.iterations = iterations self.fold_rate = fold_rate def get_win_rate(self, hand, num_opponents): return monte_carlo_win_rate(hand, num_opponents, self.iterations) def act(self, hand, pot, to_call, num_opponents): win_rate = self.get_win_rate(hand, num_opponents) actions = [('fold', 0, 0.0)] if to_call == 0: actions.append(('check', 0, 0.0)) else: actions.append(('call', to_call, ev_call(win_rate, pot, to_call))) # 设置几个加注档位 for r in [10, 30, 80]: if r > to_call: ev = ev_raise(win_rate, pot, r, self.fold_rate) actions.append(('raise', r, ev)) best = max(actions, key=lambda x: x[-1]) return best, win_rate

这个AI每次行动前都要跑一遍蒙特卡洛模拟,实际对局中如果玩家多、手数多,可能会感觉有点慢。优化方式有两个:一是把相同手牌、相同对手人数的胜率缓存起来,二是减少模拟次数到3000左右,胜率精度略有下降但速度会快很多。

3.4 一次演示:AI面对顶对和垃圾牌的决策

假设AI手牌是KKQ,对手人数1人,当前底池100,对手下注30。跑一下:

hand = ['KS', 'KH', 'QD'] pot = 100 to_call = 30 opponents = 1 bot = SimpleEVBot(iterations=10000) action, wr = bot.act(hand, pot, to_call, opponents) print(f"胜率: {wr:.3f}, 最优动作: {action}")

如果模拟正常,胜率大概在0.75以上,跟注EV约0.75×130-30≈67.5,而加注到80的EV还要算上对手弃牌带来的收益,很可能比跟注更高。AI在这里选择加注是完全合理的,因为它不仅牌强,还能通过加注逼迫对手放弃一部分底池权益。

4. 让EV模型更耐打:对手建模、方差与风险控制

4.1 对手弃牌率怎么来

上一节的ev_raise函数最关键的一个外部参数就是fold_rate。它决定了加注动作的EV里有多少来自“直接偷走底池”。这个参数不能拍脑袋定。

最简单的做法是给每个对手维护一个统计表:

  • 对手面对加注时,弃牌多少次;
  • 跟注或反加多少次;
  • fold_rate = 弃牌次数 / 总加注次数。

假设你观察到某对手连续5次面对加注都弃牌,那么下一次他面对加注的fold_rate可以暂时设成0.6以上;反过来,对方是个“加注必跟”的玩家,fold_rate就要降到0.1以下。

动态更新方式建议用滑窗统计,只保留最近20次结果。原因是玩家的行为会变化,用全量历史反而会被老策略拖累。

4.2 别被方差骗了:资金管理

EV为正只是一个长期的数学结论,短期内的波动可能远超你的预期。哪怕你有55%的胜率,连续输10把也是完全可能发生的事。

所以再好的EV模型,也必须配上资金管理规则。我的经验有一条非常实用:单次买入不要超过总资金的5%。如果你的总资金是1000,那么单场游戏最多投入50。这样即使遇到连续20场下风期,本金也不会清零,还能留在牌桌上等待正EV机会回归。

更进阶一点可以试试Kelly公式:

f* = (b × p - q) / b

其中b是净赔率(赢时收益/输时损失),p是胜率,q=1-p是败率。Kelly公式告诉你应该用多大比例的资金去下注,但这个比例往往会比较激进,实际中建议使用半Kelly,也就是把算出来的f*再除以2,降低波动。

4.3 设置策略风格:紧、松、稳

EV决策虽然统一,但可以通过阈值调整AI风格。

  • 紧:只有EV超过当前底池5%时才行动,否则弃牌。适合忌惮风险、希望减少波动的场景。
  • 松:允许EV略负也跟注,比如EV > -0.02 × pot 就上。这种AI在乱局里能抓到更多机会,但长期容易被高手利用。
  • 稳:加注时fold_rate按最保守值算,比如0.1,避免过度估计加注收益,实际对局中会更接近“不亏但赚得少”的状态。

在代码里加一个threshold参数就能控制:

def decide_threshold(action_ev, pot, style='tight'): if style == 'tight': return action_ev > 0.05 * pot if style == 'loose': return action_ev > -0.02 * pot return action_ev > 0

这个设计非常适合写成一个配置文件,方便在不同场景下测试对比。

5. 实测结果与常见坑

5.1 模拟次数不够,胜率误差大

蒙特卡洛的模拟次数直接决定胜率估算的误差。统计学上,二项分布的标准误差是:

error = sqrt(p × (1-p) / n)

当真实胜率在50%附近时,n=500的误差约2.2%,n=5000的误差约0.7%,n=10000的误差约0.5%。

如果你用500次模拟,胜率可能在48%和52%之间跳动,这会让EV的排序不稳定,甚至导致AI在临界局面反复变脸。建议最低用3000次,追求稳定用10000次。

5.2 牌型判断的边界条件

炸金花牌型判断最坑的几个点:

  • A23在很多规则里算最小顺子,也有规则算顺子但输给普通顺子?这会导致胜率计算失准。
  • 2、3、5这种散牌在部分规则里可以“吃”豹子,如果做AI前没确认规则,一定要写进代码并单独测试。
  • 同花顺和同花的判定要放在豹子之后、顺子之前,顺序错了会出大问题。

我建议在写完evaluate函数后,立刻写几个单元测试,把豹子、同花顺、同花、顺子、对子、散牌各构造一组手牌跑一遍,确保等级顺序完全符合你的预期。

5.3 随机性与可复现性

做模拟时一定要固定随机种子,或者保存每次模拟的随机状态,否则你会发现前后两次决策结果不同,无法定位问题。

random.seed(42)

这行代码在调试阶段至关重要。AI在测试环境里必须可复现,等到正式上线后再把随机种子去掉,让每次模拟引入更多随机性,减少被对手针对性抓规律的可能。

5.4 简单EV模型的边界

这套模型能赢新手,但遇到水平高的玩家会被针对。原因很简单:fold_rate是粗粒度的常数或统计值,对手可以观察你的加注规律并在你有强牌时弃牌、你示弱时偷底。真正的博弈还需要混合策略和对手动态建模,而不是每次都选“单点EV最大”的动作。

我的实际体会

我自己用这套简单EV AI跑了上千局模拟,最大的体会是:胜率估算的准确性对结果的影响,远大于决策算法的复杂度。很多AI在“牌型强度”上做得花里胡哨,结果底池赔率算不准,照样亏。炸金花这种短牌游戏,一手牌的胜率波动很大,但只要你把EV当作决策主线,就已经能超过绝大多数凭感觉打牌的玩家。

最后再分享一个小技巧:在实现时,先把加注动作的EV计算单独跑成一张表格,看看加注金额从10到100,EV曲线是怎么变的。你会发现在某个金额附近EV最高,越过那个点后加注越多反而越亏。这个峰值的出现,就是底池、胜率和对手弃牌率三者博弈的均衡点。理解了这一点,再回到代码里调fold_rate,你会对EV模型的理解深一大截。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询