简介本资源是一套面向Java游戏开发与AI算法学习者的麻将核心算法实现合集聚焦胡牌判定、AI决策、查胡校验、手牌评估及智能出牌五大关键模块适用于希望深入理解棋牌类游戏逻辑、提升算法设计与工程落地能力的中高级开发者。压缩包共52个文件含15个核心Java源码覆盖算法主干与策略类、19张PNG流程图与界面示意图如ting.png、hu.png、aiX.png等直观呈现算法逻辑、9个规则配置文本如majiang_ai_feng.txt等支持多变体规则扩展以及README.md、pom.xml、LICENSE等工程支撑文件整体36.41MB结构清晰、开箱即用。已有2713人学习下载读者可直接复用算法模块、对照图解理解状态转移与评估逻辑、基于DB与YML配置快速适配不同麻将规则并通过bat脚本与完整目录组织掌握工业级棋牌AI的分层设计思路。1. 为什么写“Java麻将算法汇总”——不是教你怎么打麻将而是帮你把胡牌逻辑从黑匣子变成可调试、可压测、可嵌入业务的确定性模块你有没有遇到过这样的场景在开发一款棋牌类教学系统时学员提交一手牌后端要秒级返回“是否可胡”“能胡几种”“哪些是有效听口”或者在做AI陪练模块时模型每轮出牌前得在毫秒内完成“手牌评估→听牌分析→风险权衡→最优出牌”四步闭环又或者测试同学甩来一个“七对子十三幺混合型”的边界用例结果服务直接抛StackOverflowError这些都不是前端交互或UI动效的问题而是底层麻将规则引擎的鲁棒性没经住真实牌局的锤炼。本篇讲的就是如何用 Java 实现一套生产可用的麻将算法集合它不追求炫技式 AI 决策树而聚焦于胡牌判定含国标/广东/日本立直等主流变体、查胡遍历、手牌评估量化、AI 出牌策略建模这五个高频刚需模块。所有代码基于纯 JDK 8无第三方规则引擎依赖支持单机高并发校验也适配微服务中轻量嵌入。如果你正在做教育类 App、棋牌陪练工具、AI 对战平台或只是想搞懂“为什么程序比人算得快还不出错”这篇就是为你写的血泪复盘。2. 胡牌算法从“递归爆栈”到“状态压缩缓存命中”的落地演进麻将胡牌判定看似简单实则是典型的组合爆炸问题。国标麻将要求“四组顺/刻 一对将”但实际需覆盖七对、十三幺、清一色、混一色、七对子、连七对、碰碰胡、全带幺、一色三节、一色四步高……共 81 种番种而每种番种对牌型结构有隐含约束。早期我们用纯递归回溯DFS实现代码简洁但致命缺陷明显面对“123456789万中中中发发发白白白”这种 13 张复杂手牌递归深度超 20 层JVM 栈空间吃紧线上偶发StackOverflowError更糟的是同一手牌反复请求时无法复用中间结果QPS 上不去。2.1 为什么选“向量位运算”而非字符串匹配常见误区是把牌面转成1m2m3m...字符串再正则匹配。但字符串操作在高频校验中开销巨大且无法利用 CPU 的位并行能力。我们改用136 位整数向量表示手牌状态万子bit 0~35每种万 1~9 各占 4 bit因最多 4 张条子bit 36~71筒子bit 72~107字牌bit 108~135中发白东南西北各占 4 bit这样一张“3万”对应 bit 8~11 置 1三张“3万”即该字段值为 3。判断“能否组成顺子 123万”只需检查(vec 0) 0xF,(vec 4) 0xF,(vec 8) 0xF是否均 ≥1。位运算比字符串切片快 17 倍JMH 测得且天然支持缓存键生成。2.2 核心胡牌判定函数带剪枝的 DFS LRU 缓存// MajiangValidator.java private static final int MAX_CACHE_SIZE 10000; private static final LoadingCacheLong, Boolean HU_CACHE Caffeine.newBuilder() .maximumSize(MAX_CACHE_SIZE) .expireAfterWrite(10, TimeUnit.MINUTES) .build(key - checkHuByDfs(key)); // key 是 handVec 的 long 值 public boolean isHu(int[] hand) { long handVec encodeHandToVector(hand); // 将 int[34] 转为 136-bit long return HU_CACHE.get(handVec); } private static boolean checkHuByDfs(long vec) { // Step 1: 快速预检 —— 总张数非14张直接false if (popcount(vec) ! 14) return false; // Step 2: 枚举将牌eye对每种可能尝试拆解剩余12张为4组 for (int i 0; i 34; i) { int count getCardCount(vec, i); if (count 2) continue; // 至少2张才能当将 long newVec removeCards(vec, i, 2); // 移除2张将牌 if (canFormFourGroups(newVec)) { return true; } } return false; }提示popcount()是 JDK 18 的Long.bitCount()若用旧版 JDK 可替换为Integer.bitCount((int)vec) Integer.bitCount((int)(vec32))。getCardCount()通过位移掩码提取某牌数量removeCards()用位减法安全扣减避免负数溢出。2.3canFormFourGroups()的关键剪枝逻辑private static boolean canFormFourGroups(long vec) { if (vec 0L) return true; // 全拆完 // 剪枝1剩余张数非3的倍数不可能拆成顺/刻 if (popcount(vec) % 3 ! 0) return false; // 剪枝2找最小非零牌贪心起点避免重复搜索 int first findFirstNonZeroCard(vec); int count getCardCount(vec, first); // 情况A用 first 当刻子需≥3张 if (count 3) { long nextVec removeCards(vec, first, 3); if (canFormFourGroups(nextVec)) return true; } // 情况B用 first 当顺子起点需 first1 和 first2 存在且同花色 if (isSameSuit(first, first1) isSameSuit(first, first2)) { int c1 getCardCount(vec, first1); int c2 getCardCount(vec, first2); if (c1 1 c2 1) { long nextVec removeCards(vec, first, 1); nextVec removeCards(nextVec, first1, 1); nextVec removeCards(nextVec, first2, 1); if (canFormFourGroups(nextVec)) return true; } } return false; }参数说明findFirstNonZeroCard()返回最低位非零牌索引0~33确保每次从最左开始拆消除顺序冗余isSameSuit(i,j)判断两张牌是否同花色如 0~8 是万9~17 是条避免跨花色顺子误判所有removeCards()调用前都校验count n防止位减为负导致逻辑错乱。3. 查胡算法穷举所有听牌组合但绝不暴力遍历全部 34 张“查胡”指给定 13 张手牌求出所有能使其胡牌的待摸牌即“听口”。暴力做法是循环 34 种牌每种补一张后调用isHu()共 34 次判定。但实际中大量补牌会导致手牌总数超 14 张如补已有的第 5 张或根本无法构成合法牌型如补字牌却无将牌白白浪费计算。我们采用逆向推导约束传播策略将平均耗时从 34×单次判定 → 5~8 次核心判定。3.1 听口分类与前置过滤先筛掉 92% 的无效候选听口本质是“补哪张牌能让手牌满足胡牌结构”。我们按胡牌结构反推约束听口类型结构要求过滤条件伪代码将牌听需补一张形成将牌hand[i] 1→ 可能听 i但需后续验证能否拆剩4组顺子听补中间/边张构成顺子hand[i]0 hand[i-1]1 hand[i1]1i 为中间hand[i]0 hand[i1]1 hand[i2]1i 为左刻子听补一张凑成刻子hand[i] 2→ 可能听 i十三幺听特殊结构仅限13种幺九牌hand中只含 1/9 万条筒 7 字牌且每种 ≤1 张public ListInteger findListeningCards(int[] hand) { ListInteger result new ArrayList(); // Step 1: 快速收集所有可能听口索引0~33 SetInteger candidates new HashSet(); // 将牌听已有1张的牌 for (int i 0; i 34; i) { if (hand[i] 1) candidates.add(i); } // 刻子听已有2张的牌 for (int i 0; i 34; i) { if (hand[i] 2) candidates.add(i); } // 顺子听检查所有可能的顺子缺口 for (int i 0; i 34; i) { if (hand[i] 0) { // 检查是否能作为顺子左/中/右 if (canBeShunziLeft(i, hand) || canBeShunziMiddle(i, hand) || canBeShunziRight(i, hand)) { candidates.add(i); } } } // Step 2: 对每个候选补牌后验证是否真能胡 for (int cand : candidates) { int[] testHand hand.clone(); testHand[cand]; if (isHu(testHand)) { result.add(cand); } } return result; }注意canBeShunziMiddle(i, hand)要求i-1和i1在同一花色且hand[i-1]1 hand[i1]1canBeShunziLeft(i, hand)要求i11 i21且三者同花色。跨花色顺子如1万2条3筒被严格禁止。3.2 十三幺专项加速用位掩码一次到位十三幺要求手牌为 {1,9}万、{1,9}条、{1,9}筒、东、南、西、北、中、发、白 共 13 种牌各 1 张再加其中任意 1 张作将。我们预计算一个THIRTEEN_YAO_MASKlong 类型其第 i 位为 1 当且仅当 i 属于十三幺牌型。查胡时只需long handVec encodeHandToVector(hand); long yaoMask THIRTEEN_YAO_MASK; if ((handVec | yaoMask) yaoMask popcount(handVec) 13) { // 确认是13张幺九枚举13种补牌 for (int i 0; i 34; i) { if ((yaoMask i) 1L) { // i 是幺九牌 int[] test hand.clone(); test[i]; if (isThirteenYao(test)) result.add(i); } } }isThirteenYao()仅检查是否含14张且全为幺九、且有且仅有一个将某牌计数为2比通用isHu()快 40 倍。4. AI 出牌算法不是“随机扔”而是基于风险-收益建模的多目标决策AI 出牌常被误解为“扔掉不能用的牌”。但真实牌局中一张牌的“无用性”是动态的当前弃 5 万可能是安全的但若下家刚打出 4 万再打 5 万就成点炮高危牌又如手握 1234567 万扔 4 万看似破顺实则保留两头听1/7 万和中间听4 万双重机会。我们的 AI 出牌模块采用三层评估体系基础牌效分 → 安全度惩罚 → 听口增益加权。4.1 牌效分Base Score量化每张牌的“结构价值”对每张牌i计算其参与顺子/刻子的可能性若i是顺子中间如 5 万且i-1和i1均存在 → 3 分若i是顺子边张如 1 万且i1和i2均存在 → 2 分若i数量 ≥2 → 可作将或刻子2 分若i是幺九牌1/9 万条筒 或 字牌且同花色其他幺九存在 → 1 分利于十三幺private int calculateBaseScore(int[] hand, int cardIndex) { int score 0; int count hand[cardIndex]; // 刻子潜力 if (count 2) score 2; // 顺子潜力检查左、中、右三种位置 if (isInSuit(cardIndex, 0)) { // 万子区 0~8 if (cardIndex 1 cardIndex 7) { // 可作中间2~8万 if (hand[cardIndex-1] 1 hand[cardIndex1] 1) score 3; } if (cardIndex 6) { // 可作左1~7万 if (hand[cardIndex1] 1 hand[cardIndex2] 1) score 2; } if (cardIndex 2) { // 可作右3~9万 if (hand[cardIndex-1] 1 hand[cardIndex-2] 1) score 2; } } // ... 条、筒、字牌类似处理 return score; }4.2 安全度惩罚Safety Penalty用“危险牌表”动态扣分我们维护一张DANGER_TABLE[34][34]DANGER_TABLE[i][j]表示若你打出牌i而玩家 j 的舍牌历史中包含i-1和i1顺子听或i出现频次高刻子听则j点炮概率提升。该表由离线统计 10 万局人类对战日志生成线上只读。出牌前对每个候选牌i计算double dangerPenalty 0.0; for (int player 0; player 3; player) { dangerPenalty DANGER_TABLE[i][player] * opponentDiscardWeight[player]; } // opponentDiscardWeight[player] 是该玩家最近3轮弃牌的权重衰减因子最终得分 baseScore - dangerPenalty * 5.0惩罚系数可调4.3 听口增益Listening Gain模拟补牌后的听口变化对每张候选牌i模拟弃掉它后计算新听口数newListeningCount并与原听口数oldListeningCount比较若newListeningCount oldListeningCount奖励 (new - old) * 10若newListeningCount oldListeningCount奖励0若newListeningCount oldListeningCount惩罚- (old - new) * 8此步骤需调用findListeningCards()但因只对当前手牌执行一次非每张候选都全量查整体耗时可控。5. 避坑那些让算法上线后半夜报警的 4 个真实翻车点现象线上服务在高并发时isHu()耗时突增至 200msCPU 持续 95%。原因HU_CACHE使用了Caffeine的expireAfterWrite但未设refreshAfterWrite导致热点手牌如清一色 13 张缓存过期瞬间大量请求穿透到checkHuByDfs()引发 GC 飙升。解决启用refreshAfterWrite(1, TimeUnit.MINUTES)让缓存后台异步刷新前台始终命中。现象某用户提交手牌[1,1,1,2,2,2,3,3,3,4,4,4,5]13 张findListeningCards()返回空列表但实际应听 5 万碰碰胡。原因canBeShunzi*()过滤逻辑错误地将5判为“不可作顺子边张”因516和527虽存在但代码中isInSuit(5,0)误判为条子区索引偏移计算错。解决重写isInSuit()用cardIndex / 9确定花色0万1条2筒3字而非硬编码区间。现象AI 在听 1 万和 9 万时连续 5 轮打出 5 万被用户投诉“AI 故意放水”。原因安全度惩罚中opponentDiscardWeight未随轮次重置导致上一轮对手弃 4/6 万的记录持续影响本轮决策过度压制中间牌。解决将opponentDiscardWeight改为每轮初始化为[1.0, 1.0, 1.0]仅对当轮对手弃牌实时衰减。现象十三幺查胡在hand [1,0,0,0,0,0,0,0,1, 1,0,...]13 张幺九时返回false但应胡。原因isThirteenYao()中检查“有且仅有一个将”时用count 2判定但未排除count 4四张相同幺九的情况后者也符合十三幺四张作将。解决将判定改为count 2 count 4并确保其余 12 种牌count 1。6. 评估算法用“听口熵”和“番种覆盖率”量化手牌质量告别玄学打分很多团队用“当前听口数”作为手牌评分唯一指标但这极易误导听 3 张1/4/7 万看似不错但若 1 万已被下家打出两次实际有效听口只剩 2 个。我们提出双维度评估法输出一个[0.0, 1.0]区间的综合质量分已在某高校棋牌教学系统中稳定运行 18 个月。6.1 听口熵Listening Entropy衡量听口的“抗干扰能力”定义听口集合L {l1, l2, ..., lk}对每个li计算其“存活概率”p(li)若li是字牌且对手未打出过该字 →p 0.95若li是数牌且对手最近 2 轮打出li-1或li1→p 0.3顺子听被破若li是数牌且对手打出过li本身 ≥2 次 →p 0.1刻子听被破其他情况 →p 0.7则听口熵H -Σ p(li) * log2(p(li))。熵值越高说明听口分布越均匀、抗干扰越强。例如听{1万, 9万}且p0.95, 0.95→H ≈ 0.28低熵风险集中听{1万, 4万, 7万, 9万}且p0.7,0.7,0.7,0.7→H ≈ 2.0高熵容错好6.2 番种覆盖率Fan Coverage预测胡牌后的理论番数上限不是直接算当前番数因未胡而是评估手牌向高番结构演化的潜力。我们预定义 12 个高价值番种及其触发条件如“清一色”需同花色 ≥10 张“七对”需 7 对对每个番种f计算其“达成度”d(f)清一色d min(1.0, sameSuitCount / 14.0)七对d pairsCount / 7.0pairsCount 是手牌中对子数量混一色d (sameSuitCount honorCount) / 14.0... 其他类似则番种覆盖率C Σ w(f) * d(f)w(f)是该番种在国标中的权重如清一色 w24七对 w24混一色 w6。C越高说明手牌越接近高番结构。6.3 综合质量分加权融合与业务适配最终质量分Q α * H β * C其中α β 1。我们根据业务场景动态调整教学模式强调稳健α0.7, β0.3→ 优先保听口多样性竞技模式强调爆发α0.3, β0.7→ 优先冲高番public double evaluateHandQuality(int[] hand, GameMode mode) { double entropy calculateListeningEntropy(hand); double coverage calculateFanCoverage(hand); double alpha (mode GameMode.LEARNING) ? 0.7 : 0.3; double beta 1.0 - alpha; return alpha * entropy beta * coverage; }我的血泪经验别迷信“AI 必须打最高分牌”。在真实对局中我曾把Q0.82的 5 万高熵但低番换成Q0.79的 1 万低熵但清一色潜力20%结果下一轮自摸清一色。后来才明白评估算法不是替你做决定而是给你一张“决策地图”——它标出哪里平坦安全、哪里陡峭高番、哪里有暗礁点炮风险。真正的高手是看着地图结合当下牌河、对手风格、剩余牌山自己踩出那一步。希望帮到你。本文还有配套的精品资源点击获取