从零构建扑克AI代理:蒙特卡洛胜率计算与对手建模实战
简介这份资源是斯坦福大学 CS 221《人工智能原理与技术》课程的最终项目实现主题为德州扑克 AI 代理面向对强化学习与博弈决策感兴趣的学习者适合具备一定 Python 基础、希望理解 MDP 建模与 Q-Learning 实战应用的中高级开发者。项目将扑克游戏抽象为马尔可夫决策过程结合 Q-Learning、蒙特卡罗模拟与 epsilon-greedy 策略训练代理先与紧攻击型、松散攻击型、紧被动型、松散被动型及随机对手对战学习再实现自我对弈或与人类玩家对战。压缩包共 8 个文件约 26KB以 3 个 py 源码文件为核心辅以 2 个 pyc 编译文件、1 个 md 说明文档及 gitignore、gitattributes 等配置项结构紧凑、便于直接运行与二次修改。目前已有 1551 人学习下载读者可从中获取完整的课程项目方案、MDP 建模思路、Q-Learning 与蒙特卡罗结合的算法实现细节以及多策略对手训练与自博弈的代码组织方式是研究强化学习在非完全信息博弈中落地的实用参考。1. 扑克 AI 代理到底在做什么从「会打牌」到「会算牌」的分水岭很多人第一次听到「扑克 AI 代理」脑子里浮现的是那种能自动帮你出牌的外挂。但真正做过一轮之后你会发现这件事的核心根本不是「替人点鼠标」而是让一个程序在信息不完整的博弈里持续做出接近最优的决策。扑克和象棋、围棋最大的区别在于你看不到对手的底牌甚至看不到发牌堆里剩下的牌。这种「不完美信息」让传统搜索类 AI 直接失效——你没法像 AlphaGo 那样把整棵树展开因为树的很多分支对你来说是隐藏的。扑克 AI 代理要解决的就是这个问题在只知道部分信息的条件下通过概率推断、对手建模和博弈论均衡计算输出一个长期期望值为正的策略。它适合两类人一类是想把强化学习、博弈论落地到具体场景的算法工程师另一类是喜欢打牌、想用代码验证自己策略到底是不是「送钱」的开发者。我见过太多人一上来就写蒙特卡洛树搜索结果跑了几十万手牌还是负收益问题就出在没搞清扑克 AI 的决策链条和参数边界。2. 从零搭一个可运行的扑克 AI 代理状态机、概率推断与决策输出2.1 先定义牌局状态别让「手牌」和「公共牌」混在一个变量里做扑克 AI 代理的第一步不是写模型而是把牌局状态抽象干净。我一般会把状态拆成四块自己的手牌、公共牌、下注轮次与筹码深度、对手的历史动作序列。很多新手会把所有牌塞进一个列表结果到了转牌圈分不清哪些是已知、哪些是未知概率计算直接崩掉。下面是一个最小可用的状态定义用 Python 的 dataclass 写字段含义我逐行注释from dataclasses import dataclass, field from typing import List, Optional dataclass class PokerState: # 自己的两张底牌用字符串表示如 As 代表黑桃A hole_cards: List[str] field(default_factorylist) # 公共牌翻牌3张、转牌1张、河牌1张按顺序追加 board_cards: List[str] field(default_factorylist) # 当前下注轮次0翻前, 1翻牌, 2转牌, 3河牌 street: int 0 # 自己的剩余筹码 my_stack: float 1000.0 # 对手剩余筹码 opp_stack: float 1000.0 # 底池大小 pot: float 0.0 # 对手本街动作序列如 [check, raise_50] opp_actions: List[str] field(default_factorylist) # 当前需要跟注的金额 to_call: float 0.0这段代码的关键在于street和board_cards的联动翻牌圈board_cards长度必须是 3转牌圈是 4河牌圈是 5。如果你在翻牌圈就拿了 5 张公共牌去算胜率那结果一定是错的。参数上my_stack和opp_stack建议用浮点数而不是整数因为后面算底池赔率时会涉及除法整数除法在 Python 里会丢精度。2.2 用蒙特卡洛模拟算胜率手牌对抗随机范围的 10 万次迭代状态定义好之后下一步是算「我这手牌现在赢的概率有多大」。最直接的方法是蒙特卡洛模拟把对手的手牌和剩余公共牌随机补全然后比大小重复十万次取频率。这里有个坑——对手的手牌不能完全随机要根据他的动作范围来加权否则算出来的胜率会虚高。先看基础版假设对手是随机手牌import random from itertools import combinations # 一副牌去掉已知牌后的剩余牌堆 def remaining_deck(known_cards): ranks 23456789TJQKA suits shdc full [r s for r in ranks for s in suits] return [c for c in full if c not in known_cards] # 简化的牌力比较只比牌型等级同等级再比关键牌 def hand_rank(cards): # 这里用简化的点数映射实际项目要写完整的5张牌评估器 rank_map {r: i for i, r in enumerate(23456789TJQKA, start2)} values sorted([rank_map[c[0]] for c in cards], reverseTrue) suits [c[1] for c in cards] is_flush len(set(suits)) 1 # 判断顺子 unique_vals sorted(set(values), reverseTrue) is_straight len(unique_vals) 5 and unique_vals[0] - unique_vals[-1] 4 if is_flush and is_straight: return (8, unique_vals) if is_flush: return (5, values) if is_straight: return (4, unique_vals) # 其余牌型简化处理按点数元组比较 return (0, values) def monte_carlo_winrate(state, iterations100000): known state.hole_cards state.board_cards deck remaining_deck(known) wins 0 for _ in range(iterations): random.shuffle(deck) # 补全对手两张手牌 opp_hole deck[:2] # 补全公共牌到5张 need_board 5 - len(state.board_cards) board state.board_cards deck[2:2 need_board] my_best hand_rank(state.hole_cards board) opp_best hand_rank(opp_hole board) if my_best opp_best: wins 1 elif my_best opp_best: wins 0.5 # 平局算半胜 return wins / iterations逻辑说明remaining_deck把已知牌剔除保证不会重复发牌hand_rank是一个极简的牌力评估器只处理同花顺、同花、顺子和高牌实际项目里你需要补上对子、两对、三条、葫芦等牌型否则胜率会偏差很大。monte_carlo_winrate每次迭代都重新洗牌模拟对手随机手牌和剩余公共牌最后用胜场数除以总迭代数。参数方面iterations设 10 万次在普通笔记本上大约跑 1 到 2 秒精度已经够用。如果你要做实时决策可以降到 1 万次误差大约在 1% 到 2% 之间。注意平局的处理算 0.5 胜比算 0 胜更接近真实期望值尤其在河牌圈双方都是高牌时平局概率不低。2.3 对手建模用动作频率反推范围而不是猜他「有没有牌」胜率算出来只是第一步真正让代理盈利的是对手建模。我见过太多人把对手当成随机手牌结果在低级别局里被跟注站打得怀疑人生。对手建模的核心是根据他历史动作的频率反推他当前手牌的范围分布。一个可落地的做法是维护一个简单的频率表记录对手在翻前加注、翻后持续下注、被加注后弃牌的次数。每打一手牌更新一次然后用这些频率去调整蒙特卡洛模拟里的对手手牌权重。class OpponentModel: def __init__(self): # 各动作的计数 self.action_counts { preflop_raise: 0, preflop_call: 0, flop_bet: 0, flop_check: 0, fold_to_raise: 0, call_raise: 0, } self.total_hands 0 def update(self, action, street): self.total_hands 1 key f{preflop if street 0 else flop}_{action} if key in self.action_counts: self.action_counts[key] 1 def get_raise_frequency(self): raises self.action_counts[preflop_raise] calls self.action_counts[preflop_call] total raises calls if total 0: return 0.5 # 没有数据时用中性先验 return raises / total这个模型很粗糙但比「完全随机」强得多。get_raise_frequency返回的数值可以直接用来加权对手的手牌范围如果对手翻前加注频率是 0.8那他在翻前加注时的手牌范围应该偏向强牌蒙特卡洛模拟里就要减少随机弱牌的比例。参数上total_hands少于 20 时建议不要过度依赖频率因为样本太小频率波动极大这时候用中性先验更稳。2.4 决策输出把胜率、底池赔率和对手频率合成一个动作有了胜率和对手模型最后一步是输出动作。最常用的框架是「期望值比较」对每个可选动作弃牌、跟注、加注估算期望值选最高的那个。跟注的期望值等于胜率乘以底池加上跟注金额再减去失败时损失的跟注金额。def decide_action(state, winrate, opp_model): pot_odds state.to_call / (state.pot state.to_call) if state.to_call 0 else 0 # 基础策略胜率高于底池赔率就跟注否则弃牌 if winrate pot_odds 0.05: # 加5%的安全边际 # 如果胜率很高且对手弃牌率高考虑加注 if winrate 0.7 and opp_model.action_counts[fold_to_raise] 3: return raise return call else: return fold这里的pot_odds是底池赔率意思是「我跟注需要付出的金额占跟注后总底池的比例」。如果胜率高于这个比例长期来看跟注是正期望。加 5% 的安全边际是为了抵消模型误差和抽水。winrate 0.7且对手弃牌次数多时加注是利用对手的弃牌倾向榨取额外价值。参数上安全边际可以根据对手的松紧程度调整对手越松边际可以越小对手越紧边际要越大。3. 避坑指南扑克 AI 代理最容易翻车的五个地方3.1 胜率算得准但动作选错底池赔率用错了分母现象代理在翻牌圈胜率 40%底池 100对手下注 50按理说跟注需要 33% 胜率应该跟但代理却弃牌了。原因pot_odds的分母写成了state.pot而不是state.pot state.to_call。底池赔率的正确公式是「跟注金额 / (当前底池 跟注金额)」漏掉跟注金额会让赔率虚高导致该跟的牌被弃掉。解决把分母补全并且在代码里加一行断言确保pot_odds永远在 0 到 1 之间。3.2 蒙特卡洛迭代次数太少河牌圈决策抖动现象同一个局面连续跑两次一次输出跟注一次输出弃牌。原因iterations设了 1000 次胜率的标准误大约在 1.5% 左右刚好卡在决策阈值附近导致动作来回跳。解决把迭代次数提到 5 万次以上或者在决策层加一个「迟滞区间」——胜率在阈值上下 2% 以内时沿用上一轮的动作避免频繁翻转。3.3 对手模型被少数极端牌局带偏现象对手连续三把翻前加注模型把raise_frequency拉到 1.0代理开始疯狂弃牌结果对手其实是在用垃圾牌偷盲。原因total_hands太小频率估计的方差极大。解决给频率加一个贝叶斯先验比如初始raise5, call5相当于先假设对手是中性玩家随着数据积累再慢慢更新。这样前 20 手牌不会因为几次极端动作就崩掉。3.4 牌力评估器漏掉踢脚牌比较导致平局误判现象双方都是顶对但代理的踢脚牌更大却被判平局。原因hand_rank只返回了牌型等级和点数元组没有在点数元组里正确排序踢脚牌。解决在比较同牌型时确保点数元组按「先比主牌、再比踢脚」的顺序排列。比如两对要返回(2, [高对, 低对, 踢脚])而不是简单地把所有点数排序。3.5 忽略位置因素按钮位和枪口位用同一套策略现象代理在枪口位用同一种范围加注被后面的人频繁反加。原因决策函数没有把位置作为输入。解决在PokerState里加一个position字段翻前范围根据位置调整——按钮位可以放宽到 40% 的手牌枪口位收紧到 15%。位置的影响在翻前尤其大翻后可以通过对手动作部分抵消但翻前必须单独处理。4. 把代理跑起来从单机模拟到批量对战的验证方法4.1 用自对战跑 10 万手牌看长期收益曲线代理写完之后最直接的验证方法是让它和自己打或者和一个固定策略的机器人打。我一般会写一个简单的循环跑 10 万手牌每 1000 手记录一次累计收益。如果曲线在 5 万手之后仍然向下说明策略有系统性问题不用再跑更多了。def self_play(agent, opponent, num_hands100000): total_profit 0 profit_curve [] for i in range(num_hands): # 模拟一手牌返回本手盈亏 profit play_one_hand(agent, opponent) total_profit profit if (i 1) % 1000 0: profit_curve.append(total_profit) return profit_curveplay_one_hand需要你根据前面的状态机和决策函数补全核心是让两个代理交替行动直到一手牌结束。profit_curve用来画图横轴是手数纵轴是累计收益。参数上num_hands至少 5 万因为扑克的方差很大短期收益曲线可能完全被运气主导。4.2 用固定种子复现问题别让随机性掩盖 bug调试扑克 AI 最头疼的是「这次输了但不知道是策略问题还是运气问题」。我的习惯是给每一手牌设一个随机种子把种子和动作序列一起打到日志里。一旦发现某手牌决策异常可以用同一个种子重放排除随机性干扰。import random def play_one_hand(agent, opponent, seedNone): if seed is not None: random.seed(seed) # 后续发牌和决策都基于这个种子 # ...参数上seed建议用「手数编号」而不是完全随机这样每一手牌都能独立复现。日志里至少记录手数、种子、自己的手牌、公共牌、每个街的动作、最终盈亏。有了这些排查问题时不用靠猜。4.3 用对手池测试泛化能力别只跟一个机器人打只跟一个固定策略对打代理很容易过拟合。我一般会准备三到五个不同风格的机器人一个超级紧的、一个超级松的、一个喜欢诈唬的、一个只会跟注的。让代理轮流跟每个机器人打 2 万手看收益曲线是否都为正。如果只对松的机器人盈利对紧的机器人亏损说明策略在对手范围收窄时没有及时调整。对手类型翻前加注频率翻后持续下注频率代理预期收益紧凶型15%70%小幅正松被动型40%30%大幅正疯狂诈唬型60%80%中等正跟注站10%20%需要调整这张表不是标准答案而是我用来判断代理是否「偏科」的参考。如果代理对跟注站的收益是负的说明它在胜率不够高时还在频繁诈唬需要把诈唬阈值调高。4.4 一个容易被忽略的细节筹码深度改变最优策略很多新手写代理时假设筹码深度固定 100BB但实际牌局里筹码深度从 20BB 到 300BB 都有。短筹码时翻前全下或弃牌的决策更常见深筹码时翻后的隐含赔率变得重要。我的做法是在PokerState里加一个stack_to_pot_ratio字段当这个比值小于 3 时启用短筹码策略模块减少翻后复杂操作优先考虑全下或弃牌。def is_short_stack(state): return state.my_stack / max(state.pot, 1) 3 def decide_action(state, winrate, opp_model): if is_short_stack(state): # 短筹码胜率超过 50% 就全下否则弃牌 return all_in if winrate 0.5 else fold # 深筹码走正常逻辑 # ...这个阈值 3 不是绝对的你可以根据自己常打的局调整。关键是意识到筹码深度是一个独立变量不能一套策略打天下。4.5 最后一步把代理放进真实牌局前先做「影子模式」在让代理真正参与决策之前我强烈建议先跑一段「影子模式」代理照常计算动作但不实际执行只把它的建议和人类玩家的实际动作做对比。跑几百手之后你会看到代理在哪些局面和人类分歧最大。这些分歧点往往就是策略需要微调的地方——要么是代理太激进要么是人类太保守但至少你能知道问题出在哪。影子模式的日志格式建议包含局面描述、代理建议动作、人类实际动作、最终结果。跑完之后按「分歧类型」分类统计如果代理在某个类型上长期跑输人类那就要回头检查那个模块的参数。我自己做扑克 AI 代理最大的教训是别一上来就追求「打败人类」先把「不犯低级错误」做到。胜率算对、底池赔率算对、对手模型别被几手牌带偏这三件事做到代理在低级别局里已经能站住了。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

代码审查实践复盘:open-code-review方案的设计与落地

代码审查实践复盘:open-code-review方案的设计与落地

open-code-review:一个让代码审查真正发挥价值的实践复盘代码审查,或者说 Code Review,是软件工程里被讨论最多、但落地最走样的环节之一。你可能见过这样的场景:需求排期紧,合并请求上了三五个评论就匆匆通过&#xf…

2026/10/13 13:38:00 阅读更多 →
新产品开发流程C0-C6:OEM/NPI门禁管理与Gate Review实战指南

新产品开发流程C0-C6:OEM/NPI门禁管理与Gate Review实战指南

简介:这是一份面向产品经理、项目经理及研发团队的新产品开发整体流程培训PPT,系统梳理了从构想、规划、设计、样品试作、工程试作、试产到量产的C0-C6七个阶段,并明确每个阶段的技术活动与产出要点,例如C2阶段强调电子、软件与工…

2026/10/13 13:38:03 阅读更多 →
DeepSeek实战手册:环境配置、LoRA微调与推理排错全指南

DeepSeek实战手册:环境配置、LoRA微调与推理排错全指南

简介:本资源是清华大学新媒体研究中心出品的《DeepSeek:从入门到精通》专题课件,面向AI开发者、算法工程师及大模型应用实践者,系统解析国产推理大模型DeepSeek-R1的技术定位、能力边界与工程落地逻辑。课件以PPTX格式呈现&#x…

2026/10/13 13:38:07 阅读更多 →

最新新闻

glibc内存管理核心:malloc/free原理、RSS异常排查与调优实践

glibc内存管理核心:malloc/free原理、RSS异常排查与调优实践

搞了十多年Linux服务端,我遇到过不少内存方面的疑难杂症:程序跑着跑着RSS飙到十几个GB,看代码却找不到任何泄漏;压力测试一过进程就被OOM干掉;线上明明有内存空闲,服务却越跑越慢。这些问题表面上是业务逻辑…

2026/10/14 0:15:46 阅读更多 →
SpringBoot2+Vue3构建医院资源管理系统实战

SpringBoot2+Vue3构建医院资源管理系统实战

1. 项目概述与核心价值医院资源管理系统作为医疗信息化建设的核心组成部分,其技术选型与架构设计直接关系到医疗机构的运营效率。这套基于SpringBoot2Vue3MyBatis-PlusMySQL8.0的技术栈组合,代表了当前企业级应用开发的主流技术方向。我在三甲医院信息化…

2026/10/14 0:15:46 阅读更多 →
easy-peasy Reducer TypeScript API:用 Reducer<State, Action> 为 Redux 风格 Reducer 添加完整类型

easy-peasy Reducer TypeScript API:用 Reducer<State, Action> 为 Redux 风格 Reducer 添加完整类型

前端 【免费下载链接】easy-peasy Vegetarian friendly state for React 项目地址: https://gitcode.com/gh_mirrors/ea/easy-peasy 点击查看 免费下载 本篇围绕 easy-peasy 的 TypeScript API 中的 Reducer 类型展开,讲解如何在模型声明中为“标准 Red…

2026/10/14 0:15:46 阅读更多 →
EFK日志监控系统从零搭建:Elasticsearch+Filebeat+Kibana实战指南

EFK日志监控系统从零搭建:Elasticsearch+Filebeat+Kibana实战指南

直接说结论:如果你团队里缺一套能随时查线上日志、能统计错误趋势、能按关键字秒级定位问题的系统,EFK三件套是目前最稳妥的选择。Elasticsearch负责存储和检索,Filebeat轻量采集日志,Kibana负责可视化。三者合起来,就…

2026/10/14 0:15:46 阅读更多 →
二叉树指定结点层次计算:递归与迭代实现及工程实践

二叉树指定结点层次计算:递归与迭代实现及工程实践

1. 从一道经典题说起:为什么"求结点层次"值得单独拎出来讲但凡刷过二叉树相关题目的人,大概率都见过这道题:给一棵二叉树,再给一个目标结点,问这个结点在第几层。很多人第一反应是"这不就是遍历的时候顺…

2026/10/14 0:15:46 阅读更多 →
Lorenz吸引子实战指南:从混沌方程到三维相图可视化

Lorenz吸引子实战指南:从混沌方程到三维相图可视化

1. 为什么一张“蝴蝶翅膀”能搅动整个非线性科学界?你可能在科普视频里见过它:两条螺旋状轨迹,像一对缓缓张开的蝴蝶翅膀,在三维空间里永不停歇地缠绕、分离、再靠近——它不是艺术渲染,而是Lorenz系统在特定参数下自动…

2026/10/14 0:14:45 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →