大家好,失踪已久的九日哥回来了
由于前段时间一直专注于开发鉴黄,所以身心都有些不适,故也给了自己很长的放松时间~
然而回来了之后,九日哥毅然决然的选择了棋牌(dubo)事业~看来我这辈子也就离不开黄赌毒了。
这就是我的职业生涯规划,恩,看起来没什么不对。
首先带来的就是麻将胡牌、听牌的算法,不过大家都知道,麻将各个地方的规则都不同,所以相关算法也需要作出一定的调整。
先简单说一下本次demo的规则要求把。
1.不计番,也就是没那么多胡法,最后胡了就行。
2.胡牌结构满足4*3+2即可,也就是4套组合(一个组合3张牌)+一个对子,组合可以是顺,也可以是碰。并且不要求必须有碰或者顺,可以是七对
3.有混牌(混牌就是什么都算,相当于癞子),本章demo不会实现,后续应该还会写几篇,敬请关注~
4.牌型有34种,即3*9=27基本牌型外加东南西北中发白7个风牌
大概就是这么多了 ,首先说明本章主要算法就是动态规划以及回溯,具体算法介绍可以参考以下博客
http://blog.csdn.net/sm9sun/article/details/53244484 回溯
http://blog.csdn.net/sm9sun/article/details/53240542 DP动态规划
另外说明以下代码只是初步的小demo,并未经过大量测试,极有可能出bug,敬请关注后续博客
废话不多说了,首先,我们要先思考一个问题,就是先算听牌还是先算胡牌?
这个问题很重要。如果你的算法用于听牌(13张),也就是你要返回相应的可以胡的牌的序列然后根据此序列判断接下来是否胡牌。相反,如果你的算法用于胡牌(14张)那么实际你听牌是采取手上的牌+1张枚举所有牌去检查是否可以胡牌。
看起来算胡牌的话每次听牌都要进行34次枚举实验或许会浪费时间,但是仔细想想,其实胡牌也有相应的好处,就是可以迅速剪枝掉不符合胡牌的情况。举个例子,如果你手上有1个一万,没二万,听牌算法还要考虑你是否有三万以及三万与后面牌型的关系。而胡牌算法简单粗暴返回一个false即可。
两种算法究竟哪个更好取决于其的剪枝程度,现在我还无法给出确定的结论,不过从我目前的思维来说,我更倾向于胡牌算法。
一些定义
/*#define 0~8 万#define 9~17 饼#define 18~26 条#define 27 东#define 28 南#define 29 西#define 30 北#define 31 中#define 32 发#define 33 白 *//*组牌信息* 采取状态压缩方法,对于每个牌类给出状态(0~4)* */var Mahjongtiles_info = {userID:-1, //对应用户IDin_Pai:new Array(34), //手里的牌序列out_Pai:new Array(34), //外面的牌序列sum_Pai:new Array(34), //总和的牌序列};/*对局信息*/var Mahjonggame_info={player:Array(4), //四个用户对应的组排hunPai:-1, //混牌zhuangjia:0, //庄家PaiList:new Array(136), //麻将队列};
注:PaiList为麻将队列,其还需初始化以及乱序等功能性函数
/*麻将队列初始化*/function Init_List(arr){var index=0;for(var i=0;i<34;++i){arr[index++]=i;arr[index++]=i;arr[index++]=i;arr[index++]=i;}}/*麻将队列乱序*/function shuffle_List(arr) {var i = arr.length, t, j;while (i) {j = Math.floor(Math.random() * i--);t = arr[i];arr[i] = arr[j];arr[j] = t;}}
那么采用胡牌算法的话,处理听牌就很简单了,枚举所有牌型
/*判断是否可以听*/function CanTingPai(arr,TingArr){var ret=false;var result=false;for(var i = 0; i < 34; ++i){// if(arr[i]<4) { 如果该牌玩家已经有4张了 是否还可以听此张 有待商榷arr[i]++;ret = CanHuPai(arr);arr[i]--;// }if(ret){result=true;TingArr.push(i);}}return result;}
注:arr为状态记录的牌型数组,例如:arr[0]=3表示一万有三张
通过枚举调用CanHuPai函数验证,我们先写出相对简单的七小对胡牌规则进行验证
/*是否为七对*/function CanHuPai__7pair(arr){var pairCount=0;/*七对*/for(var k in arr) {var c = arr[k];if (c == 2) {pairCount++;}else if (c == 4) {pairCount += 2;}else if( c != 0) //当c不满足0,2,4情况即有单张出现,直接返回false{return false;}}if(pairCount==7){return true;}else{return false;}}
/*判断是否可以胡,枚举几类胡法*/function CanHuPai(arr){if(CanHuPai__7pair(arr)){return true;}else if(CanHuPai_norm(arr)){return true;}else{return false;}}
由于是初步的demo,先写出七小对和普通牌型的分支。
input:
function SetTest(arr){arr[1]+=2;arr[2]+=2;arr[3]+=2;arr[4]+=2;arr[5]+=4;arr[9]+=1;}
output:
9
true
然后是如何算胡牌。
第一步,先剔除对子使其成为3*4牌型。剔除对子的策略:
如果arr[i]==2即一对且其周围无法与其组合成一对顺子那么可以直接把其当成一对进入判胡环节
举个例子,我有两个二万,但是我一万+三万+四万总共不足4个,即无论怎么组合我这两个二万都没办法成为两组顺子
那么我这两个二万只能当做一对了,至于后面能不能胡先不管。
而arr[i]>2时则需要考虑 是否可以直接跳过而不进行剔除一对的判断。原理一样
比如arr[i]==4即有四个一样的,如果将其中两个剔除,另外两个不足与周围组合成两个顺子,那么显然这四个一样的是不可以拆成2+2的
arr[i]==3需要判断周围是否可以满足一个顺子
具体代码如下:
/*针对于arr[i]=2情况的剪枝处理*/function Cancutpair_2(arr,i){if(i>26) //如果为风牌则直接可以剔除对子{return true; //true为可以直接剔除,false为还需回溯}else if(i==0||i==9||i==18) //一万 一饼 一条{if(arr[i+1]>=2&&arr[i+2]>=2) //如果对应的二与三都大等于2{return false;}else{return true;}}else if(i==8||i==17||i==26) //九万 九饼 九条{if(arr[i-1]>=2&&arr[i-2]>=2) //如果对应的七与八都大等于2{return false;}else{return true;}}else if(i==1||i==10||i==19) //二万 二饼 二条{if(arr[i-1]+arr[i+1]+arr[i+2]>=4&&arr[i+1]>=2) //如果一+三+四大于4且三大于2{return false;}else{return true;}}else if(i==7||i==16||i==25) //八万 八饼 八条{if(arr[i-1]+arr[i+1]+arr[i-2]>=4&&arr[i-1]>=2) //如果九+七+六大于4且七大于2{return false;}else{return true;}}else if(arr[i-1]+arr[i+1]+arr[i-2]+arr[i+2]>=4) //如果相邻的两端大于四张牌{return false;}else{return true;}}/*针对于arr[i]=3情况的剪枝处理,与 Cancutpair_2相反,当相邻牌数小于两张牌,则不可取*/function Cancutpair_3(arr,i){if(i>26) //如果为风牌则不可以成为对子{return false;}else if(i==0||i==9||i==18) //一万 一饼 一条{if(arr[i+1]>=1&&arr[i+2]>=1) //如果对应的二与三都大等于1{return true;}else{return false;}}else if(i==8||i==17||i==26) //九万 九饼 九条{if(arr[i-1]>=1&&arr[i-2]>=1) //如果对应的七与八都大等于1{return true;}else{return false;}}else if(i==1||i==10||i==19) //二万 二饼 二条{if(arr[i-1]+arr[i+2]>=1&&arr[i+1]>=1) //如果一+四大等于1且三大等于1{return true;}else{return false;}}else if(i==7||i==16||i==25) //八万 八饼 八条{if(arr[i+1]+arr[i-2]>=1&&arr[i-1]>=1) //如果九+六大等于1且七大等于1{return true;}else{return false;}}else if(arr[i-1]+arr[i+1]+arr[i-2]+arr[i+2]>=2) //如果相邻的两端大于两张牌{return true;}else{return false;}}/*针对于arr[i]=4情况的剪枝处理,与 Cancutpair_3相似,由于多出来两个,故当相邻牌数小于四张牌,则不可取*/function Cancutpair_4(arr,i){if(i>26) //如果为风牌则不可以成为对子{return false;}else if(i==0||i==9||i==18) //一万 一饼 一条{if(arr[i+1]>=2&&arr[i+2]>=2) //如果对应的二与三都大等于2{return true;}else{return false;}}else if(i==8||i==17||i==26) //九万 九饼 九条{if(arr[i-1]>=2&&arr[i-2]>=2) //如果对应的七与八都大等于2{return true;}else{return false;}}else if(i==1||i==10||i==19) //二万 二饼 二条{if(arr[i-1]+arr[i+2]>=2&&arr[i+1]>=2) //如果一+四大等于2且三大等于2{return true;}else{return false;}}else if(i==7||i==16||i==25) //八万 八饼 八条{if(arr[i-2]+arr[i+1]>=2&&arr[i-1]>=2) //如果六+九大等于2且七大等于2{return true;}else{return false;}}else if(arr[i-1]+arr[i+1]+arr[i-2]+arr[i+2]>=4) //如果相邻的两端大等于4{return true;}else{return false;}
同时,在判断胡牌前,加入剔除对子的调用
function CanHuPai_norm(arr){var count =0; //记录手牌总数for(var i = 0; i < 34; ++i) {count += arr[i];}/*剔除对子*/var ret=false;for(var i = 0; i < 34; ++i){if(arr[i]==2){if (Cancutpair_2(arr, i)){arr[i] -=2; //直接剔除ret = CanHuPai_3N_recursive(arr,count-2,0);arr[i] +=2;return ret;}else {arr[i] -=2;ret = CanHuPai_3N_recursive(arr,count-2,0);arr[i] +=2;if(ret) //如果满足可以返回,不满足还需要尝试其他的对子{return ret;}}}else if(arr[i]==3){if (Cancutpair_3(arr, i)){arr[i] -=2;ret = CanHuPai_3N_recursive(arr,count-2,0);arr[i] +=2;if(ret){return ret;}}}else if(arr[i]==4){if (Cancutpair_4(arr, i)){arr[i] -=2;ret = CanHuPai_3N_recursive(arr,count-2,0);arr[i] +=2;if(ret){return ret;}}}}return ret;}
好了,也就是说接下来我们只剩下一个判断剩下的牌是否可以组成n个组合了,也就是回溯调用的CanHuPai_3N_recursive
先说明以下参数,function CanHuPai_3N_recursive(arr,count,P) arr为牌型数组,count为剩余手牌数,count==0时表示可以胡,P代表遍历当前牌ID的下标
也就是说,假设我P=8(九万)就意味着前面的1~8万我都处理了,都是0了,9万就不用进行更多的考虑了,如果9万是1张或者2张或者4张,那么直接返回false(因为对子已经剔除
1张或2张或4张又不可能与其他组合成一组)
/*采取DP动态规划的思想*递归尝试消一组牌(3张),当arr所有值即count都为0时,即可以胡牌*count为剩余牌数* 当遇到冲突时,即不可以胡牌* */function CanHuPai_3N_recursive(arr,count,P) {// process.stdout.write(arr +'\n'+count+'\n'+P+'\n');var ret=false;if(count==0){return true;}if(P>26) // 风牌只能组成碰{if(arr[P]==3){ret=CanHuPai_3N_recursive(arr,count-3,P+1);return ret;}else if(arr[P]==0){ret=CanHuPai_3N_recursive(arr,count,P+1);return ret;}else{return false;}}if(arr[P]==0){ //如果没有该牌,直接跳过进行下一张ret=CanHuPai_3N_recursive(arr,count,P+1);}if(arr[P]==1){if(P%9>6) //如果该牌是八或者九,则无法组合顺,不能胡{return false;}else if(arr[P+1]>0&&arr[P+2]>0) //能组合成顺{arr[P]--;arr[P+1]--;arr[P+2]--;ret=CanHuPai_3N_recursive(arr,count-3,P+1);arr[P]++;arr[P+1]++;arr[P+2]++;}else //无法组合顺,不能胡{return false;}}if(arr[P]==2){ //与1同理,组成两对顺if(P%9>6){return false;}else if(arr[P+1]>1&&arr[P+2]>1){arr[P]-=2;arr[P+1]-=2;arr[P+2]-=2;ret=CanHuPai_3N_recursive(arr,count-6,P+1);arr[P]+=2;arr[P+1]+=2;arr[P+2]+=2;}else{return false;}}if(arr[P]==3){ret=CanHuPai_3N_recursive(arr,count-3,P+1); //当前需求 三对顺等同于三对碰/* if(P%9>6) { ret=CanHuPai_3N_recursive(arr,count-3,P+1); } else if(arr[P+1]>2&&arr[P+2]>2) { arr[P]-=3; arr[P+1]-=3; arr[P+2]-=3; ret=CanHuPai_3N_recursive(arr,count-9,P+1); arr[P]+=3; arr[P+1]+=3; arr[P+2]+=3; if(!ret) { arr[P + 1] += 3; arr[P + 2] += 3; ret=CanHuPai_3N_recursive(arr,count-3,P+1); arr[P + 1] -= 3; arr[P + 2] -= 3; } } else { ret=CanHuPai_3N_recursive(arr,count-3,P+1); } */}if(arr[P]==4) { //如果为四张,则至少有一张与后面组成为顺,剩下的递归,P不变if(P%9>6){return false;}else if (arr[P + 1] > 0 && arr[P + 2] > 0) {arr[P]--;arr[P + 1]--;arr[P + 2]--;ret = CanHuPai_3N_recursive(arr, count - 3, P );arr[P]++;arr[P + 1]++;arr[P + 2]++;}else{return false;}}/* console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'); process.stdout.write(arr +'\n'); console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'); */return ret;}
其他测试需要的代码
var start = new Date();var aMahjongtiles_info=Mahjongtiles_info;SetArrZero(aMahjongtiles_info.in_Pai);SetTest(aMahjongtiles_info.in_Pai);//var ret=CanHuPai(Mahjongtiles_info.in_Pai);//process.stdout.write(aMahjongtiles_info.in_Pai +'\n');var Tinglist=new Array();var ret=CanTingPai(Mahjongtiles_info.in_Pai,Tinglist);process.stdout.write(Tinglist +'\n');console.log(ret);var end = new Date();console.log(start,end,end-start);
我们做一下测试吧,有一种牌型叫做九莲宝灯,即3个一万,3个九万加上二~八万各一张,其可以听所有的万子。
input
/*指定测试样例*/function SetTest(arr){arr[0+9]+=3;arr[1+9]+=1;arr[2+9]+=1;arr[3+9]+=1;arr[4+9]+=1;arr[5+9]+=1;arr[6+9]+=1;arr[7+9]+=1;arr[8+9]+=3;}
output
9,10,11,12,13,14,15,16,17
true
2017-03-23T12:32:45.654Z 2017-03-23T12:32:45.701Z 47
然后我们将其改一下,变成
arr[9+9]+=3;arr[1+9]+=1;arr[2+9]+=1;arr[3+9]+=1;arr[4+9]+=1;arr[5+9]+=1;arr[6+9]+=1;arr[7+9]+=1;arr[8+9]+=3;
即一饼变成了一条
output
9,10,12,13,15,16
true
2017-03-23T12:33:34.041Z 2017-03-23T12:33:34.086Z 45
单吊二五八饼,边一四七饼 三六九饼不胡 答案应该是正确的~
恩,今天就到这里了,这只是一个初步的demo 代码+思路总共都是今天一天搞的,所以可能会有很多的问题
接下来会做更多剪枝方面的优化、BUG修改。包括混子牌什么的~
后续的优化处理帖
http://blog.csdn.net/sm9sun/article/details/77773277