最近在做一款叫做“卡五星”的三人麻将,来自湖北,麻将里只有筒和条(没有万)以及中发白这些牌。
其他的特殊功能暂且不提,其中有一个需求是玩家听牌后需要将与胡牌有关系的牌显示出来给其他玩家看。
举个例子,比如说我的手牌是1234677筒,此时我胡5筒(4,6),那么就要讲4筒,6筒显示出来。又比如7888筒(胡6,7,9筒),就要讲7888都显示出来。
放组样例图:
自己玩家视角
其他玩家视角
开始接到这个需求时候我是懵逼的,我们都知道大部分麻将胡牌算法都是将这个牌算进手牌中然后判断是否处于胡的状态,至于具体怎样判胡请参考之前的博客
http://blog.csdn.net/sm9sun/article/details/65448140
所以我们很难快速的定位究竟哪些牌是跟胡牌有关系的,如果你胡5筒,且你有4筒6筒,但是你不能确定就是4,6夹5筒,也有可能是234 67这样的牌型。
最起始的思路就是先把胡牌周围的牌拉出来,比如说5,如果手牌34同时存在的情况下,把34拉进来,如果46同时存在把46拉进来,如果67同时存在把67拉进来。同时5也要拉进来,因为可以是单吊或者对对胡等。但是又考虑到23467这样的牌型,所以把胡牌周边的牌拉进来的同时,也要把周边牌的周边也拉进来,因为你也不知道这些牌是跟胡的牌组成一个组合还是另有组合。那这就是一个扩展搜索的思路。至此,如果此思路实现了,那么至少我胡5筒时23467就都会显示出来,而不是显示3467。即使234是多余的,对于用户来说,也可以通过牌型分析出该玩家胡的牌。
//深度关联和胡牌有关系的牌function dfs_Mingholds(PartMingholds, ismingMap, countMap,OldMingholds){if (OldMingholds.length == 0){return PartMingholds;}else{var NewMingholds = [];for (var k in OldMingholds) {var c = OldMingholds[k];for (var i = 0; i < 34; i++) {if (i - c == 0 && !ismingMap[i] && countMap[i] > 0) {for (var j = 0; j < countMap[i]; j++) {PartMingholds.push(i);NewMingholds.push(i);}ismingMap[i] = true;}else if (i - c == 2 && !ismingMap[i] && countMap[i] > 0&&i!=9&&i!=10) {if (countMap[i - 1] > 0) {for (var j = 0; j < countMap[i]; j++) {PartMingholds.push(i);NewMingholds.push(i);}ismingMap[i] = true;}}else if (i - c == -2 && !ismingMap[i] && countMap[i] > 0 && i!=7&&i!=8) {if (countMap[i + 1] > 0) {for (var j = 0; j < countMap[i]; j++) {PartMingholds.push(i);NewMingholds.push(i);}ismingMap[i] = true;}}else if (i - c == 1 && !ismingMap[i] && countMap[i] > 0 && i != 9) {if (i - 2 >= 0) {if (countMap[i - 2] > 0 && !ismingMap[i]) {for (var j = 0; j < countMap[i]; j++) {PartMingholds.push(i);NewMingholds.push(i);}ismingMap[i] = true;}}if (i + 1 < 18) {if (countMap[i + 1] > 0 && !ismingMap[i]) {for (var j = 0; j < countMap[i]; j++) {PartMingholds.push(i);NewMingholds.push(i);}ismingMap[i] = true;}}}else if (i - c == -1 && !ismingMap[i] && countMap[i] > 0 && i != 8) {if (i + 2 < 18) {if (countMap[i + 2] > 0 && !ismingMap[i]) {for (var j = 0; j < countMap[i]; j++) {PartMingholds.push(i);NewMingholds.push(i);}ismingMap[i] = true;}}if (i - 1 >= 0) {if (countMap[i - 1] > 0 && !ismingMap[i]) {for (var j = 0; j < countMap[i]; j++) {PartMingholds.push(i);NewMingholds.push(i);}ismingMap[i] = true;}}}}}if (NewMingholds.length > 0){return PartMingholds = dfs_Mingholds(PartMingholds, ismingMap, countMap,NewMingholds);}else{return PartMingholds;}}}
简单说明一下,PartMingholds为记录部分明牌的序列数组,ismingMap是存储该编号牌是否已经处于明牌状态,比如说我胡36,那么3会关联45,6也会关联45,防止重复计算。countMap是手牌,countMap[i]=k表示手里有k张第i号牌。OldMingholds是上一级维护的明牌序列。也就是当前层级搜索的范围。
麻将的编号说明:0-8表示筒,9-17表示条,18-26表示万(这里没有),中27 发28 白29 东30 南31 西32 北33 (东西南北也没有)
因为条和中并不连续,所以在做顺子关联判断是唯一要区分的就是筒和条,所以我们看到判断里会加上i!=8等限制。如果当前层级没有关联新的元素,即NewMingholds的长度为0,那么我们就返回PartMingholds即可,否则需要根据新关联的元素继续搜索。开始讲胡牌序列作为OldMingholds引入。
至此为止,我们已经过滤掉和胡牌毫无关系的牌了,那么接下来,我们就将这个PartMingholds稍微优化一下,剔除不需要显示的牌。
首先就是顺子的剔除,我们以顺子中间的牌为基准,什么样的顺子我们可以直接剔除呢?
1:将明牌序列排序后,如果该顺子位于序列的两端,且不两端的扩展牌不胡。那么就可以直接剔除。比如说,排完序后我的左端是4,5,6且我不胡3,4。那么就可以认为这4,5,6必定要组成一组且跟胡牌没有关系。右端同理。
2:如果该顺子的有一端不连续且也不胡扩展边的这张牌,那么其也一定是固定的一组,比如说2245677。已知4的左侧不是3,且也不胡3,也不胡4,那么4,5,6必然要组成顺子。此牌该显示2277
//顺子剔除for (var i = 1; i <= PartMingholds.length - 2; ++i) {if (PartMingholds[i + 1] - PartMingholds[i] == 1 && PartMingholds[i] - PartMingholds[i - 1] == 1) {if ((PartMingholds[i] >= 1 && PartMingholds[i] <= 7) || (PartMingholds[i] >= 10 && PartMingholds[i] <= 16)) {if (((i == 1) && (tinglist.indexOf(PartMingholds[i] - 2) < 0) && (tinglist.indexOf(PartMingholds[i] - 1) < 0))|| ((i == PartMingholds.length - 2) && (tinglist.indexOf(PartMingholds[i] + 2) < 0) && (tinglist.indexOf(PartMingholds[i] + 1) < 0))) {PartMingholds.splice(i - 1, 3);}else {if ((PartMingholds[i - 2] < PartMingholds[i] - 2) && (tinglist.indexOf(PartMingholds[i] + 2) < 0) && (PartMingholds.indexOf(PartMingholds[i] + 2) < 0)&& (tinglist.indexOf(PartMingholds[i] - 2) < 0)) {PartMingholds.splice(i - 1, 3);}else if ((PartMingholds[i + 2] > PartMingholds[i] + 2) && (tinglist.indexOf(PartMingholds[i] - 2) < 0) && (PartMingholds.indexOf(PartMingholds[i] - 2) < 0)&& (tinglist.indexOf(PartMingholds[i] + 2) < 0)) {PartMingholds.splice(i - 1, 3);}}}}}
将无关联的顺子剔除后,我们进一步的判断牌型,首先是尝试寻找不能组成顺子的两张牌,因为很有可能就是这两张牌是我们最终要显示出来的牌。很简单, 我们遍历整个明牌的序列,如果只有2张牌无法与其他的牌组成顺子,那么一定就是他俩了,因为他俩得跟胡的那张牌组成一个组合。我们暂且称这两张牌为cp牌
for (var i = 0; i < PartMingholds.length; ++i) {for (var j = i + 1; j < PartMingholds.length; ++j) {for (var k = j + 1; k < PartMingholds.length; ++k) {if ((PartMingholds[j] - PartMingholds[i] == 1 && PartMingholds[k] - PartMingholds[j] == 1)) {if (PartMingholds[j] != 9 && PartMingholds[k] != 9 && PartMingholds[i] < 18) {is_cp[i] = false;is_cp[j] = false;is_cp[k] = false;}}}}if (is_cp[i]) {delMinghold.push(PartMingholds[i]);cp_count++;}}
在cp牌计算完后,如果出现cp_count大于2的情况,那么一定是坎或者对子也在关联牌中,我们将其剔除。
//坎牌剔除var t = 0;var ti = -1;if (cp_count > 3) {for (var i = 0; i < delMinghold.length - 2; ++i) {if (delMinghold[i] == delMinghold[i + 1] && delMinghold[i] == delMinghold[i + 2]&& delMinghold.length % 3 == 2){t++;ti = i;}}if (t == 1 && tinglist.indexOf(delMinghold[ti]) < 0) {cp_count -= 3;delMinghold.splice(ti, 3);}}//对子剔除var p = 0;var pi = -1;if (cp_count > 2) {for (var i = 0; i < delMinghold.length - 1; ++i) {if (delMinghold[i] == delMinghold[i + 1]) {p++;pi = i;}}if (p == 1 && tinglist.indexOf(delMinghold[pi]) < 0) {cp_count -= 2;delMinghold.splice(pi, 2);}}
这里可能有人不理解,对牌剔除需要判断p是否等于1,因为一副胡牌最后不能有2对,坎牌为什么也要判断呢?因为如果坎牌存在2个以上且都跟胡牌相关,那么其一定会组成特殊的牌型,比如说6777888或者5556777等。并且需要判断 delMinghold.length % 3 == 2。因为如果余数是1,那么这个坎牌一定是参与胡牌了,比如7888。因为如果不关联在之前的处理就已经过滤掉了,比如2555这样。只有余数是2的情况,才能剔除,因为剩下的内两个牌可以做成胡口。
最后我们判断cp的状态:
如果cp_count == 2且只胡一张牌,那么必定是夹胡或者边37胡,否则如果这两张cp牌不等,那么就是两端胡。例如34胡25这样。如果两张cp牌相等,那么必定是对对胡,且另一个对于其他牌型可以连成顺,比如2233456这样。
如果cp_count == 1且只胡一张牌,那么一定是夹胡且另一端可以连成顺,比如24567这样。
除此之外的牌型太过复杂,应该都会有所关联,那么将整个PartMingholds显示出来即可。
if (cp_count == 2) {if (tinglist.length == 1) {return delMinghold;}else if (delMinghold[0] != delMinghold[1]) {return delMinghold;}else if (tinglist.length == 2 && (delMinghold[0] == tinglist[0] || delMinghold[0] == tinglist[1])) {return [tinglist[0], tinglist[0], tinglist[1], tinglist[1]];}else {return PartMingholds;}}else if (cp_count == 1) {if (tinglist.length == 1) {if (tinglist[0] - delMinghold[0] == 1) {delMinghold.push(tinglist[0] + 1);return delMinghold;}else if (tinglist[0] - delMinghold[0] == -1) {delMinghold.push(tinglist[0] - 1);return delMinghold;}else {return PartMingholds;}}else {return PartMingholds;}
补充一些变量的说明:
var cp_count = 0; //cp牌的数量var delMinghold = []; //剔除后的明牌序列var is_cp = []; //判断该编号牌是否为cpvar tinglist = []; //听牌序列
另外可以加一些常见牌型的优先处理,比如57这样的夹胡,或者67这样的两端胡,以及七小对神马的
//优先处理,提高效率if (tinglist.length == 1) {var p1 = tinglist[0] + 1;var p2 = tinglist[0] - 1;if (PartMingholds.indexOf(p1) > 0 && PartMingholds.indexOf(p2) > 0 && (p1 != 9 && p1 != 10 && p2 != 8 && p2 != 7)) {return [p1, p2];}}if (tinglist.length == 2 && ((tinglist[1] - tinglist[0]) == 3 || (tinglist[0] - tinglist[1]) == 3)) {var p1 = tinglist[1] > tinglist[0] ? tinglist[1] - 1 : tinglist[0] - 1;var p2 = p1 - 1;if (PartMingholds.indexOf(p1) > 0 && PartMingholds.indexOf(p2) > 0 && (p1 < 8 || p2 > 9)) {return [p1, p2];}}
其他特殊牌型方法类似,就不一一写出来了。
……if (k.pattern == "7pairs"){return [parseInt(k)];}……
运算前必须将 PartMingholds排好序,is_cp等相关状态数组初始化。
至此这部分算法就大功告成了,目前还没有发现特殊牌型的bug,但是从逻辑上来说不是100%的缜密。在之前测试的过程中也是经过了反复的修改。也一度很崩溃很绝望。我在【以明牌序列为主干进行剔除处理】还是以【听牌个数为主干进行分支处理】这两种方式纠结了很久,最终算是中和了一下,勉强过关。
这个需求优先级并不高,所以也不可能花太多时间去完成,等闲下来时我会再想想,或许会想到更好的解决方案吧。