江西麻将胡牌算法 全解
无论你是棋牌游戏开发者、算法学习者,还是对麻将感兴趣的玩家,这篇文章都能帮你彻底搞懂江西麻将的胡牌判断逻辑。
一、江西麻将胡牌算法简介
江西麻将作为中国地方麻将的重要分支,以南昌麻将最具代表性。南昌麻将最大的特色在于「精牌」(也叫宝牌、癞子),大大提升了胡牌的灵活性和趣味性。
胡牌算法是棋牌游戏开发中最核心的模块之一,负责判断玩家当前的手牌是否满足胡牌条件。本文将从基础规则到进阶优化,为你呈现一套完整、可靠且高效的江西麻将胡牌算法方案。
▲ 南昌麻将精牌(宝牌)玩法示意
二、江西麻将胡牌基本规则
2.1 牌型构成
以南昌麻将为例,使用万、条、筒三种花色,每种花色1~9各4张,共108张牌。部分地区玩法会加入风牌(东南西北中发白),扩展为136张。
- 万:1万 ~ 9万,各4张
- 条:1条 ~ 9条,各4张
- 筒:1筒 ~ 9筒,各4张
2.2 胡牌公式
标准胡牌需满足:n × AAA + m × ABC + DD,其中:
- AAA = 刻子(三张相同牌)
- ABC = 顺子(三张同花色连续牌)
- DD = 将牌(两张相同牌)
- n + m = 4,即一共4副面子 + 1对将牌
2.3 特殊胡牌牌型
- 七对子:7个对子组成(共14张)
- 碰碰胡:4个刻子 + 1对将牌
- 精牌(宝牌):可替代任意牌,使胡牌更容易
▲ 胡牌牌型结构示意图
三、算法核心逻辑
胡牌算法的本质是搜索与回溯。我们将手牌编码为计数数组,通过递归尝试所有可能的将牌、刻子、顺子组合,判断是否存在一种划分方式,使牌型满足胡牌条件。
3.1 数据结构
使用长度为27(或34)的整数数组表示手牌:
- 索引 0~8:万(1万~9万)
- 索引 9~17:条(1条~9条)
- 索引 18~26:筒(1筒~9筒)
- 索引 27~33(可选):风牌
数组的每个位置存储该牌的张数(0~4)。
3.2 递归回溯思路
- 先检查七对子(快速判断)
- 遍历每一种牌型,尝试将其作为「将牌」
- 将牌去除后,递归检查剩余牌是否全部由刻子+顺子组成
- 递归时遇到不满足条件就剪枝回溯
▲ 递归回溯判断流程图
四、算法实现步骤
下面按步骤拆解完整的胡牌判断流程:
牌型编码
将手牌字符串转换为计数数组
统计数量
统计每种牌的张数
特殊牌型
检测七对子、碰碰胡
遍历将牌
尝试每个对子作为将牌
递归判断
检查剩余牌是否为面子
精牌处理
精牌可替代任意牌
4.1 牌型编码示例
例如:手牌「1万2万3万 7筒7筒7筒 3条4条5条 南南南 5万5万」
编码后数组为:[1,1,1,0,1,0,0,0,0, 0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0, ...]
五、代码示例(JavaScript)
下面是一段完整的胡牌判断算法实现,支持普通胡牌、七对子、碰碰胡,并预留了精牌处理接口。
/**
* 江西麻将胡牌算法
* 支持普通胡牌、七对子、碰碰胡
* 牌型编码:0-8万,9-17条,18-26筒
*/
const WAN = 0,
TIAO = 9,
TONG = 18;
/**
* 将手牌数组转换为计数数组
* @param {number[]} tiles 手牌数组(已排序)
* @returns {number[]} 长度34的计数数组
*/
function countTiles(tiles) {
const cnt = new Array(34).fill(0);
for (const t of tiles) {
cnt[t]++;
}
return cnt;
}
/**
* 主判断函数
* @param {number[]} tiles 手牌(14张)
* @returns {boolean} 能否胡牌
*/
function isHu(tiles) {
const cnt = countTiles(tiles);
// 1. 七对子检测
if (isQiduizi(cnt)) return true;
// 2. 碰碰胡检测
if (isPengPengHu(cnt)) return true;
// 3. 标准胡牌检测
return isStandardHu(cnt);
}
// 七对子:7个对子
function isQiduizi(cnt) {
let pairs = 0;
for (let i = 0; i < 34; i++) {
if (cnt[i] % 2 !== 0) return false;
pairs += cnt[i] / 2;
}
return pairs === 7;
}
// 碰碰胡:4个刻子 + 1对将
function isPengPengHu(cnt) {
// 尝试每种将牌
for (let i = 0; i < 34; i++) {
if (cnt[i] < 2) continue;
let temp = [...cnt];
temp[i] -= 2;
let ok = true;
for (let j = 0; j < 34; j++) {
if (temp[j] % 3 !== 0) {
ok = false;
break;
}
}
if (ok) return true;
}
return false;
}
// 标准胡牌:n副刻子 + m副顺子 + 1对将
function isStandardHu(cnt) {
// 尝试每种将牌
for (let i = 0; i < 34; i++) {
if (cnt[i] < 2) continue;
const temp = [...cnt];
temp[i] -= 2;
if (canFormMianzi(temp)) {
return true;
}
}
return false;
}
// 递归判断剩余牌是否全部由面子组成
function canFormMianzi(cnt) {
// 找到第一张非0牌
let first = -1;
for (let i = 0; i < 34; i++) {
if (cnt[i] > 0) {
first = i;
break;
}
}
// 所有牌都组完
if (first === -1) return true;
// 尝试刻子
if (cnt[first] >= 3) {
cnt[first] -= 3;
if (canFormMianzi(cnt)) {
cnt[first] += 3;
return true;
}
cnt[first] += 3;
}
// 尝试顺子(需同花色且连续)
if (first < 27 && first % 9 <= 6 &&
cnt[first + 1] > 0 && cnt[first + 2] > 0) {
cnt[first]--;
cnt[first + 1]--;
cnt[first + 2]--;
if (canFormMianzi(cnt)) {
cnt[first]++;
cnt[first + 1]++;
cnt[first + 2]++;
return true;
}
cnt[first]++;
cnt[first + 1]++;
cnt[first + 2]++;
}
return false;
}
// 示例测试
const tiles = [
WAN + 0, WAN + 0, WAN + 0,
WAN + 1, WAN + 2, WAN + 3,
TONG + 1, TONG + 1, TONG + 1,
TIAO + 5, TIAO + 6, TIAO + 7,
WAN + 8, WAN + 8
];
console.log(isHu(tiles)); // true 或 false
💡 精牌(宝牌)处理:在精牌场景中,可将精牌数量单独标识,在递归时优先尝试精牌替代。具体实现可将 canFormMianzi 增加一个参数 wildcard,当实际牌不足时使用精牌补齐。
六、常见问题解答(FAQ)
七、结语
江西麻将胡牌算法是棋牌开发中的重要基础,涵盖搜索、回溯、剪枝、状态表达等经典计算机算法思想。通过本文的详细讲解和代码示例,相信你已经对胡牌判断有了全面而深入的理解。
在实际项目中,还需要结合精牌、算番、听牌提示等进阶功能进一步完善。如果你在学习和开发中遇到问题,欢迎在评论区留言交流!