资讯动态

麻将游戏AI源代码实战:牌型编码、胡牌检测与出牌决策系统解析

发布时间:2026/10/25 15:05:58 来源:尧图企业网站定制
简介基于C/C实现的麻将游戏AI源代码面向Windows平台可编译运行核心在于电脑AI的智能对战算法。适合对游戏开发与人工智能应用感兴趣、具备一定C基础的程序员作为学习完整游戏架构和AI决策逻辑的实践案例。压缩包共87个文件约586KB其中33个mp3音频、17个bmp图片、12个h头文件与11个cpp源文件构成主体辅以工程配置、图标和编译描述代码与资源分离清晰便于按模块研读。已有3820人学习适合通过源码理解麻将牌型建模、胡牌判断、碰杠操作等规则实现以及与AI的搜索策略和评估函数结合的人机对战机制。项目还涵盖Win32窗口消息处理、GDI绘图、事件驱动、数据结构运用和调试优化等要点能帮助读者将底层系统交互与上层游戏逻辑串联起来无论牌型判定还是AI出牌决策都可以结合代码逐步跟踪学习。1. 麻将游戏AI源代码不是玄学是一套可拆解的牌桌决策系统麻将游戏AI源代码这个词听起来像某种「一键智能打牌」的玄学实际拆开就是让程序完成四件事认牌、判胡、算进张、选安全牌。我第一次接这类需求是在一个棋牌游戏项目里甲方要求AI在1秒内完成出牌还不能明显比新手菜。当时从开源社区扒了几份C老代码要么牌型编码看不懂要么只顾进攻不管防守改起来比重新写还累。这个方向真正值钱的部分不在算法多新潮而在于牌型编码、胡牌检测、进张计算和出牌评分这四块怎么严密地咬合。适合谁想做棋牌机器人、研究博弈AI入门、或者给自建游戏加陪玩NPC的开发者照着下面这条路径走能少踩至少一半的坑。2. 麻将AI的核心决策链从牌型编码到向听数计算先把骨架立住2.1 牌型编码34计数数组为什么比136序列更适合AI做决策写麻将AI的第一步不是写策略而是定义牌在程序里长什么样。新手最容易犯的错误是把一手牌存成一个由136张牌ID组成的列表比如[0, 1, 2, 3, 8, 8, 9, ...]这样。136序列的好处是直观摸一张牌就是在列表里加一个元素但它有两个致命问题第一判断「手里有没有某张牌」要线性扫描第二枚举所有可能的拆牌组合时同一张牌的不同实例会被当成不同分支产生大量重复计算。比如手里有2万三张拆刻子时第一张、第二张、第三张分别被选中逻辑上完全等价但递归会多跑三倍。我习惯用34长度的计数数组hand[i]表示第i种牌的张数。0到8是万9到17是条18到26是筒27到33是字牌。这个编码天然去重同一种牌的张数用数值表示枚举牌型时只关心「用了哪几种牌、各用几张」不关心具体哪一张。网络检索里那些C写的开源麻将源码核心牌型表示也几乎全是这一套思路区别只是接口封装好不好看。# 34牌编码0-8万9-17条18-26筒27-33字牌 def init_hand(cards): hand [0] * 34 for c in cards: hand[c] 1 return hand # 判断第i张牌能否作为顺子的起点 def can_start_shunzi(i): # i % 9 6 表示该花色中还有2个空位 # 字牌区间27-33不参与顺子 return i 27 and i % 9 6参数说明init_hand接收一个从0到33的牌ID列表返回计数数组。can_start_shunzi是后面胡牌检测的基础工具函数它把「万条筒三种花色、每种9张」的边界条件锁死——一旦i落在字牌区间27到33或者i是该花色的第7、8、9张它就不会被错误地当成顺子起点。这里藏着一个高频坑如果把字牌当成花色的一部分参与顺子检测胡牌判定会把「东南西」这种根本不存在的顺子拆出来导致AI产生幻觉胡牌。34编码还有另一个好处就是牌河信息和手牌信息可以用同一个remain数组来表达。remain[i]表示第i种牌还剩多少张初始为4每出现一张已知牌就减1。后面所有概率计算都建立在remain之上34编码让「已知牌扣减」这个操作变成一次数组下标访问非常快。2.2 胡牌检测与向听数一手牌离胜利还有几步这才是决策的标尺胡牌检测是地基。标准麻将胡牌结构是4个面子加1对雀头面子可以是顺子或刻子。实现上主流是回溯法先尝试从手牌里拆出一副面子递归拆剩下的直到拆空。def can_win(hand): if sum(1 for x in hand if x 2) 7: return True # 七对特判 for i in range(34): if hand[i] 2: hand[i] - 2 # 枚举雀头 if check_regular(hand): hand[i] 2 return True hand[i] 2 return False def check_regular(hand): if all(x 0 for x in hand): return True for i in range(34): if hand[i] 0: # 优先拆刻子 if hand[i] 3: hand[i] - 3 ok check_regular(hand) hand[i] 3 if ok: return True # 再尝试顺子 if can_start_shunzi(i) and hand[i] 0 and hand[i1] 0 and hand[i2] 0: hand[i] - 1; hand[i1] - 1; hand[i2] - 1 ok check_regular(hand) hand[i] 1; hand[i1] 1; hand[i2] 1 if ok: return True return False # 这张牌既不能拆刻也不能拆顺直接失败 return False逻辑说明can_win先特判七对然后用两层循环枚举雀头的位置把雀头从手牌里拿走剩下的交给check_regular判断能否全拆成面子。check_regular的递归顺序很重要——我习惯先拆刻子再拆顺子因为刻子只有一种拆法能尽早确定唯一路径顺子拆法多留到后面尝试。return False放在循环末尾是剪枝关键一旦发现某张非0牌既凑不成刻子也凑不成顺子整条路径立刻报废不用再往后试。参数说明hand是34计数数组函数执行时会修改它所以调用方要在递归前做好备份或者在递归里保证每次修改后复原。代码里用加回操作来还原这是回溯的标准写法。如果想提速把hand转成tuple后用functools.lru_cache装饰check_regular单局内大量重复子结构会被缓存命中性能能提升好几倍。向听数比胡牌判定更重要它回答的问题是「一手牌还差几次有效换牌才能听」。向听数为0表示已经听牌1表示一向听。计算向听数有三种级别暴力BFS枚举所有换牌组合结果最准但14张牌时可能跑出百万级节点贪心估算把牌拆成面子、搭子、对子速度极快但边界情况会算错标准算法用回溯加剪枝兼顾速度和准确度。我一般把暴力BFS当作对照基准生产环境用DFS剪枝def calculate_shanten(hand): best 8 def dfs(h, depth, last_i): nonlocal best if depth best: return if can_win(h): best depth return for i in range(last_i, 34): if h[i] 0: h[i] - 1 dfs(h, depth 1, i) h[i] 1 dfs(hand[:], 0, 0) return best这个实现里有三个关键参数depth是已经打出的牌数last_i用来保证每一层只往牌号更大的方向打避免「先打1万再打2万」和「先打2万再打1万」这种重复分支best是全局最优值一旦当前深度超过best就剪枝。它比标准算法慢不少但作为正确性验证工具足够。标准向听数算法的核心公式是向听数等于2乘以「还缺的面子数」再减去雀头修正实现时用「面子数 搭子数」的贪心拆牌法这个我建议单独写一个fast_shanten函数和暴力版本互相校验。为什么向听数这么重要AI的进攻决策全部围绕它展开打出某张牌后向听数降低说明手牌在进步向听数不变但牌更安全说明在防守。向听数没算对后面所有评分公式都是在错误的地基上盖楼。我见过好几个项目AI表现忽好忽坏最后定位到是向听数边界case算错导致AI在即将听牌时打出一张完全无关的牌。3. 用Python跑通最小麻将AI三个函数搞定听牌判断与出牌决策3.1 听牌判断与进张数把「打哪张」变成可计算的数值听牌判断就是「摸到哪张牌能胡」的枚举。在34编码下做法是对34种牌逐一尝试加一张然后调用can_win。能胡牌的那几张就是听牌列表。def get_ting_list(hand): ting [] for i in range(34): if hand[i] 4: # 理论上一共只有4张 hand[i] 1 if can_win(hand): ting.append(i) hand[i] - 1 return ting def calc_remaining(hand, discards): remain [4] * 34 for i in range(34): remain[i] - hand[i] for d in discards: remain[d] - 1 return remainget_ting_list的时间复杂度是34次胡牌检测配合第2章的递归剪枝在普通机器上跑一次大约几毫秒。calc_remaining负责统计「每张牌还剩多少张」它从每样4张的初始值开始先扣掉手牌再扣掉discards里已经露面的牌。返回值是后面所有概率计算的基础——一般来说remain值越小说明这张牌已经被消耗得越多后续摸到的可能性越低。这里引入一个关键概念进张数。听牌列表里有三张牌不代表摸到其中任何一张的概率是3/34因为每张牌在牌池里的剩余数量不同。我习惯把听牌列表对应的remain值累加起来叫作剩余进张数def calc_advance(hand, remain): ting get_ting_list(hand) return sum(remain[t] for t in ting)这个数字比「听几张」准确得多。举个例子AI听2万和5万2万已经被人打掉3张5万一张未现如果单纯数听牌数会认为两者等价但实际进张数2万只剩1张、5万还有4张差距是4倍。这两者的区别直接决定了出牌决策的质量。注意remain参数必须基于当前桌面的真实可见牌如果自己手牌里已经有2张2万那么remain[2万]还要再扣掉这两张否则会把已经在自己手里的牌当成潜在摸牌目标——这种错误在刚写AI时特别容易犯。3.2 出牌评分不搜索先让AI学会算概率有了听牌判断和进张数出牌决策就水到渠成对每一张可以打出的牌假设打出它然后评估打完后手牌的三项指标——进张数、安全度、向听数变化。把三个指标加权求和选分数最高的打出。这是大多数据说「够用」级别的规则AI的通用做法。def evaluate_discard(hand, discards): remain calc_remaining(hand, discards) candidates [i for i in range(34) if hand[i] 0] scores {} for c in candidates: hand[c] - 1 # 模拟打出这张牌 ting get_ting_list(hand) attack_score sum(remain[t] for t in ting) defense_score (4 - remain[c]) * 0.5 shanten calculate_shanten(hand) scores[c] attack_score * 1.0 defense_score * 1.0 - shanten * 2.0 hand[c] 1 # 还原手牌 return sorted(scores.items(), keylambda x: x[1], reverseTrue)逻辑说明attack_score是打出c之后手牌还能听多少张剩余牌代表进攻潜力defense_score用「c这张牌已经见了几张」来近似安全度见得多说明别人手里也不太可能有打出去被胡的风险低shanten项是向听数惩罚AI应该避免打出让手牌变散的牌。三个1.0/1.0/2.0的系数就是AI的性格开关。参数说明candidates用[i for i in range(34) if hand[i] 0]生成而不是遍历34张手牌ID列表是为了避免同一手牌重复评估。defense_score的系数0.5意味着一个完全安全但进张数为0的牌打分只有2.0封顶可能输给一张进张数很高但稍带危险的牌。这个比例不是固定的第4章会专门讲怎么调。这里故意没有引入蒙特卡洛模拟或者深度搜索原因有二第一麻将的决策空间虽然不小但多数情况下「进张数安全度向听数」已经能覆盖八成以上的出牌质量剩下的提升来自读牌和特殊牌型这部分用规则补齐比引入重型搜索更划算第二蒙特卡洛在实时场景下时间开销难控制一旦超时AI就会卡住不动这在棋牌项目里是不可接受的。等你把规则引擎调到接近上限再考虑用神经网络做进一步替代那是第6章的事。3.3 最小闭环随机开局跑一局看AI每一步怎么选有了听牌判断和出牌评分最小闭环就是把一副洗好的牌堆发给AI让它摸一张打一张直到胡牌或流局。这个闭环能跑通说明你的骨架没问题。import random def run_episode(seed42): random.seed(seed) tiles [i for i in range(34) for _ in range(4)] random.shuffle(tiles) hand init_hand(tiles[:14]) wall tiles[14:] discards [] while wall: draw wall.pop() hand[draw] 1 # 摸牌 if can_win(hand): return win, len(wall) ranked evaluate_discard(hand, discards) play ranked[0][0] discards.append(play) hand[play] - 1 # 出牌 return draw, 0 print(run_episode(seed42))run_episode的参数seed是随机种子固定种子可以让每次实验得到完全相同的发牌序列方便对比不同策略参数的表现。返回值的第二个元素是「胡牌时牌墙还剩几张」也就是第几巡胡牌数字越小说明胡得越早。discards列表会传给evaluate_discard来计算remain但这里有一个简化——它只记录了AI自己打出的牌没有记录虚拟对手的弃牌所以remain会偏高。要获得更真实的概率需要至少模拟一个对手的完整动作但作为最小闭环这个简化可以接受。注意这里没有任何碰杠吃的逻辑AI只做摸牌和出牌两件事。如果你的目标是验证核心决策链这个环境已经足够如果要做完整的四人麻将需要再加三方的状态模拟和副露处理那属于架构扩展核心函数不需要改动。这种步步为营的推进方式能让你在写策略之前先确认地基牢固而不是一口气堆出几百行代码然后调试到崩溃。4. 参数调优与策略选择进张、安全度、防守的三方博弈4.1 进攻权重与防守权重的配比三个数字决定AI性格evaluate_discard里的三个系数本质上是把「进攻」「防守」「牌型紧凑度」三个目标加权到一个分数里。我见过不少项目在这里翻车原因都是把attack_score的权重设得太高AI每张牌都往听牌最快的方向打结果中盘开始疯狂打危险张连续点炮。反过来防守权重太高也不行AI会为了不点炮拆掉自己的听牌整局打得像缩头乌龟——低级失误的频率是低了但整场下来赢不了牌。我的经验是权重不能是固定的三个常数而应该随牌局阶段动态变化。开局阶段大家手牌都是散的打生张的惩罚很低进攻权重可以高到了终盘牌池里危险牌堆积对手随时可能听牌防守权重必须压过进攻。def dynamic_weights(draw_count, max_draws18): # draw_count是摸牌数巡数max_draws是预计最长巡数 progress min(draw_count / max_draws, 1.0) attack 1.5 - progress * 0.5 defense 0.5 progress * 1.5 return attack, defense这个函数把progress当作0到1的阶段进度attack从1.5线性降到1.0defense从0.5线性升到2.0。调用时把第3章评分公式里的固定系数替换成这两个动态值即可。调参时不要凭感觉我一般把progress分成5档0.0/0.25/0.5/0.75/1.0每档跑100局自对弈统计胜率选胜率最高的那一组曲线。注意这两条曲线的交叉点通常在progress等于0.6到0.7之间意味着AI大部分时间还是偏进攻的只在残局转入防守。这符合牌理前期落后了还能追后期点一炮可能直接出局。4.2 危险度计算的颗粒度从「剩余张数」到「对手牌河」上一章的defense_score用的是静态安全度这张牌在牌池里出现得越多越安全。这个指标在开局阶段够用但到了中后盘必须加入对手行为的动态信息。核心思路是读牌从对手打过的牌反推他手里大概有什么、听什么。热词里反复出现的「AI Agent」「多AI协作」在这个场景的映射就是让防守模块不再只是被动算概率而是像老玩家一样对牌河做语义分析。麻将读牌有几条经验规则比如「对手刚打过5万那他手里的3万和7万很可能留不住2万和8万反而是点炮高发牌」再比如「对手碰了9万那他手里基本不会有8万」。这些规则用代码实现并不复杂关键是要维护每名对手的弃牌历史def danger_level(card, opponent_discards): # 基础对手打出过这张牌越多次他越不要这张牌打出去越安全 safety opponent_discards.count(card) danger 4 - safety # 筋牌修正对手打过5那么2和8的打牌风险上升 suit card % 9 if suit 1 and (card 1) in opponent_discards: danger 1 if suit 7 and (card - 1) in opponent_discards: danger 1 return danger这段代码的筋牌逻辑写得比较粗糙真实读牌需要同时维护「对手打过的所有牌」和「对手碰杠吃过的牌」并且用牌河安全表而不是简单的加减分。但方向是对的danger_level返回的数值不再是一张牌的静态属性而是随着对手动作动态变化的信号。我在项目里会把danger_level的结果乘到原先的defense_score上也就是final_defense defense_score * (1 - danger_level / 8)这样危险牌的防守分会迅速衰减AI就不太可能去送炮。这里要提醒一句读牌规则本质是概率推断不是定理。对手打过5万2万和8万风险高只是统计意义上的相关不代表每次都会点炮。所以danger_level的修正幅度不要设太大尤其是对付那些打法混乱的随机对手读牌规则反而会帮倒忙。我踩过的坑是把筋牌修正权重设成2.0结果AI在残局过度害怕把把打安全牌进攻节奏全丢了。4.3 时间预算与搜索剪枝普通电脑上1秒内出牌的优化手段如果AI要上桌实时出牌1秒是底线。evaluate_discard对每个候选牌都要做get_ting_listget_ting_list内部又是34次胡牌检测乘积效应下CPU会吃紧。我用的优化手段有三个。第一个是缓存胡牌检测结果。把hand转成元组后用lru_cache装饰can_win相同手牌结构只算一次。单局内大量出牌评估会碰到重复的子结构命中率很高实测能把单次出牌耗时从800毫秒压到200毫秒左右。第二个是剪枝候选牌数量。并非每一张可打牌都值得评估先按「手牌中张数多的优先」做一个快速排序只评估前10张候选。这个启发式在大多数情况下不会漏掉最优解因为最优出牌几乎总是来自手牌中孤立的、远离搭子的那些牌。张数多的牌通常已经在成型组合里打出去的机会成本太高。第三个是提前退出。如果某张候选牌打出后直接听牌向听数为0直接选中它不再评估其他牌。听牌是阶段性目标能听就听不要为了贪图更高的进张数放弃听牌。best_play candidates[0] for c in candidates: hand[c] - 1 if calculate_shanten(hand) 0: best_play c hand[c] 1 break # 直接听牌立即停止搜索 hand[c] 1这段提前退出的代码要注意必须在还原手牌后再break否则后续逻辑会拿到被修改的手牌。实际项目中我会把候选按花色分组如果某个花色已经凑齐面子那这个花色的牌可以先跳过集中评估还有缺陷的花色。这类剪枝对性能的提升很直接而且不容易引入策略错误。说到底时间预算的本质是让AI在1秒内做出最不坏的决定而不是理论上最优的决定——牌桌上是实时博弈慢半拍的AI再聪明也没人愿意跟你玩。5. 麻将AI的避坑指南常见翻车现场与排查手段5.1 现象一七对子与字牌顺子让胡牌检测判错现象AI手里明明已经是七对子的听牌形态程序却判定没听打出一张字牌后退出听牌。日志里手牌是7个对子can_win却返回False。原因can_win里虽然写了七对特判但特判条件写错位置。比如「七对」要求7个对子而手牌是14张但如果碰过牌有副露手牌数量不是14张特判会误伤。另一个高频错误是check_regular里字牌的顺子边界处理字牌区间27到33如果不加阻断递归时会把「东南西」当作顺子拆出来导致胡牌结果完全错乱。解决副露状态要单独处理。我的做法是胡牌检测函数接收一个extra参数表示已经副露的面子数。副露了k组面子普通牌型就只需要检测「剩余手牌能否凑出4-k组面子1对雀头」七对子则直接禁用七对不能有副露。字牌边界用第2章的can_start_shunzi函数统一判断不要在每个递归里手写边界条件否则改一处漏三处。排查这个问题时最快的方法是打印debug信息在can_win入口打印hand和副露状态在check_regular出口打印拆出来的面子组合一张一张对错误基本是秒现。5.2 现象二AI已经听牌下一手又自己退听现象AI打出某张牌后听牌下一轮摸牌后评分函数给出的最优出牌居然把听牌拆了向听数从0变成1AI在明明该进攻的时候选择了拆牌防守。原因评分函数里的shanten惩罚系数是2.0如果某张牌打了之后进张数很高、防守分很高总分压过「保持听牌」的收益AI就会犯迷糊。这是典型的软约束问题——听牌状态应该是一个硬约束而不是被其他分数盖过的加权项。解决在评分前先判断当前手牌是否听牌如果听牌任何会导致退听的出牌直接排除if calculate_shanten(hand) 0: score {} for c in candidates: hand[c] - 1 if calculate_shanten(hand) 0: hand[c] 1 continue # 跳过这张会让AI退听的牌 ting get_ting_list(hand) attack sum(remain[t] for t in ting) defense (4 - remain[c]) * 0.5 score[c] attack defense hand[c] 1 return max(score, keyscore.get)这个硬约束的优先级必须最高因为「听牌后退听」在真实牌桌上是最容易被玩家看穿的破绽。我当初用自对弈日志发现AI频繁退听加了这个约束之后胜率立刻提升了大约8个百分点——听起来不多但在麻将这种高随机性游戏里差两个点就已经很可观了。5.3 现象三碰杠牌没算进剩余张数AI以为安全实际点炮现象AI打出某张牌自评防御分很高认为剩余张数很少结果下家正好碰牌或胡牌。检查日志发现对手碰过的那张牌在remain里没有扣除。原因calc_remaining只减去了hand和discards没有统计碰杠吃产生的明牌。碰牌后的三张牌属于已知牌必须从remain里扣掉否则AI会误以为牌池里还有这张牌低估它的风险。解决维护一个完整的visible列表把场上所有已经翻开的牌都放进去包括所有对手的副露和弃牌。calc_remaining改为从4张初始值扣掉hand、discards、exposed_cards三部分的计数。我建议写一个统一的函数def calc_remaining(hand, discards, exposed): remain [4] * 34 for i in range(34): remain[i] - hand[i] for d in discards: remain[d] - 1 for e in exposed: remain[e] - 1 return remainexposed里每张牌出现的次数就是它在牌桌上已经亮相的次数。这里要特别小心重复扣除如果一张牌既在discards里又在exposed里说明记录员把它记了两遍。我的做法是exposed里只放副露的牌discards只放打出的牌两者物理上互斥。这类计数错误不会导致AI崩盘但会让它的防守判断时好时坏属于最阴险的隐性bug——表面上代码没问题就是偶尔点炮。5.4 现象四带万能牌宝牌/金牌的规则AI牌型计数直接爆炸现象在带万能牌规则的麻将里比如武汉麻将的皮、日本麻将的宝牌指示AI的胡牌判定会漏判或者递归函数卡死表现为出牌时CPU飙满、迟迟不落子。原因万能牌能当作任何一张牌用如果把它硬编码成34种牌之一can_win的递归分支数会指数膨胀。比如手里有一张万能牌枚举它当1万、当2万、当3万……每个分支都要递归拆一次手牌越多越卡。解决把万能牌从34计数数组里单独拿出来用一个wild_count参数表示。check_regular递归时优先用普通牌凑面子凑不齐再尝试用万能牌补位def check_regular_wild(hand, wild): if all(x 0 for x in hand) and wild 0: return True if wild 0: return False for i in range(34): if hand[i] 0: if hand[i] 3: hand[i] - 3 ok check_regular_wild(hand, wild) hand[i] 3 if ok: return True if can_start_shunzi(i) and hand[i] 0 and hand[i1] 0 and hand[i2] 0: hand[i] - 1; hand[i1] - 1; hand[i2] - 1 ok check_regular_wild(hand, wild) hand[i] 1; hand[i1] 1; hand[i2] 1 if ok: return True return False return wild 2 # 兜底万能牌补雀头这个函数的性能问题可以用剪枝缓解只有当前递归层没有任何普通牌可以组成面子时才考虑动用wild牌。很多人觉得万能牌是加分项其实它是坑——一旦处理不当AI的听牌判断会把「根本不可能的胡牌牌型」也算进去导致AI选出一张看似能胡实则永远胡不了的牌。判断万能牌AI强度时我建议把带wild的规则和无wild的规则分开评测否则两种规则下的参数会互相干扰。6. 用自对弈验证AI强度一套不上桌就能测出水平的评测方法6.1 固定种子自对弈胜率、听牌率、平均向听数三个指标调参数不能靠感觉我习惯把评测脚本当作项目的第一份源码来写。固定的做法是选10个随机种子每个种子跑100局统计胜率、听牌率、放炮率和平均向听数。听牌率衡量AI有没有在流局前进入听牌状态放炮率衡量防守水平平均向听数衡量手牌推进效率这四个数字比单看胜率靠谱得多——因为麻将发牌的随机性太大几十局的胜率可能全是噪声。def evaluate(games100): win 0 for seed in range(games): result run_episode(seedseed) if result[0] win: win 1 return win / games这个函数演示了胜率的统计方式。要统计听牌率和放炮率需要把run_episode的返回值扩展成一个dict在每巡记录AI是否听牌、是否出过危险张、最终是否点炮。口播一下我的经验听牌率稳定在80%以上说明进攻决策合格放炮率如果超过15%防守权重需要上调。平均向听数是更细的指标它可以告诉你AI在每一巡的手牌质量是在变好还是停滞。评测脚本一定要跟着源代码一起做版本管理。我见过太多人改了一版出牌评分感觉好像变强了实际上没有数据支撑。把评测脚本写进CI流程里每次提交代码都自动跑一轮出牌评分的回归问题在合入分支前就能发现而不是上线后被打爆了才回头查。这也是「源代码管理」这个词在AI项目里最实在的落地场景——管理的不只是代码还有代码带来的行为变化。6.2 进阶从规则引擎到神经网络的接棒点规则引擎的极限出现在读牌逻辑变厚的时候。当你有二三十条读牌规则AI的行为开始变得僵硬继续堆if-else的收益很低这时候适合引入神经网络做打牌决策。我的建议是走「模仿学习自对弈强化」的路线先用当前规则AI跑出大量对局把状态手牌编码、牌河编码、弃牌历史和AI打出的牌存成样本训练一个简单的全连接网络做行为克隆再用自对弈让网络通过策略梯度继续进化。# 数据采集跑1000局规则AI保存状态和决策 state encode_state(hand, discards, opponents) record.append((state, play))encode_state的编码方式要跟训练网络的输入对齐常见做法是手牌34维、牌河34维、副露情况再拼接几维总共一百维以内。这个规模不用上大模型一个三层的MLP就能完成从「模仿老师」到「超越老师」的过程。不过训练之前一定要确认规则AI本身没有系统性缺陷否则网络只会学着把错误发扬光大。说到底我自己的教训是先有扎实的规则引擎和评测体系再谈AI进阶顺序反了会陷入无穷无尽的调参循环。早期我把大量时间花在调花哨的读牌规则上结果没有一个可靠的评测标准改一版翻一版。后来老老实实把自对弈脚本写在项目第一行每次改动先跑一轮再上桌验证翻车的概率才真正压下来。希望这套方法也能帮你把时间花在真正能提升AI强度的地方而不是在玄学调参里打转。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑