node.js——麻将算法(一)基本判胡
首先带来的就是麻将胡牌、听牌的算法,不过大家都知道,麻将各个地方的规则都不同,所以相关算法也需要作出一定的调整。
先简单说一下本次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, //对应用户ID
- in_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修改。包括混子牌什么的~
node.js——麻将算法(一)基本判胡相关推荐
- node.js——麻将算法(五)胡牌算法的一些优化处理方案(有赖子版)
以前有赖子判胡算法 http://blog.csdn.net/sm9sun/article/details/65632646 以前的帖子说明了处理赖子的两种方案:枚举代替及插空补缺,并最终选择了枚举遍 ...
- node.js——麻将算法(四)胡牌算法的一些优化处理方案(无赖子版)
回想三月份刚接触棋牌时写过一些麻将的算法,转眼间半年过去了,回顾下曾经的代码,写的还真是蛮low的 http://blog.csdn.net/sm9sun/article/details/654481 ...
- node.js——麻将算法(三)胡牌相关明牌
最近在做一款叫做"卡五星"的三人麻将,来自湖北,麻将里只有筒和条(没有万)以及中发白这些牌. 其他的特殊功能暂且不提,其中有一个需求是玩家听牌后需要将与胡牌有关系的牌显示出来给其他 ...
- node.js——麻将算法(六)简易版麻将出牌AI1.0
普通麻将的出牌AI如果不是要求特别高的话,其实蛮容易实现的,毕竟大多数人打牌都只是看自己的手牌. 所以作为简易版的AI,出牌的策略只要奔着胡牌去就可以了.我们能想到的就是把相邻或相同的牌凑到一起,把单 ...
- node.js——麻将算法(二)赖子玩法
上篇博客传送门http://blog.csdn.net/sm9sun/article/details/65448140 上文中已经实现了基本胡法的算法,本章加入"癞子玩法"的判胡逻 ...
- node.js——麻将算法(七)简易版麻将出牌AI2.0
*文本为上一篇博客http://blog.csdn.net/sm9sun/article/details/77898734的部分追加优化 上一篇博客已经实现了基本的出牌逻辑,大部分情况能够给出正确的策 ...
- 麻将算法(七)胡牌之对子判断
判断一手牌中的牌是否可以胡牌,思路是选出将手牌中的对子,将手牌中的对子牌"去掉"后(将这个牌添加到一个叫DUIZI的list里),将去掉对子的手牌进行连牌与同牌判断 ,将" ...
- 基于QT和Node.js的八叉树算法提取图片主题色
资源下载地址:https://download.csdn.net/download/sheziqiong/85883609 资源下载地址:https://download.csdn.net/downl ...
- Node.js复制/删除服务器端文件到指定目录文件夹下,并且预判是否存在该目录,如果没有,则递归创建该文件夹目录
注意,前情提示: 本代码基于<Node.js(nodejs)对本地JSON文件进行增.删.改.查操作(轻车熟路)> 传送门Node.js(nodejs)对本地JSON文件进行增.删.改.查 ...
最新文章
- C语言获取当前工作路径
- 笔记-项目质量管理-编制质量管理计划的工具与技术
- cassandra连不上,报Stop listening for CQL clients, Failed managing commit log segments
- SpringBoot 注解大全
- Hbase中的列式表映射到hive的外表
- 单片机c语言pwm整流的程序,基于 单片机控制PWM整流电源的设计.doc
- qt最大化和还原实现_研究进展 | 水生所关于细菌异化型硝酸盐还原成铵与反硝化脱氮两种途径抉择的分子调控机制研究取得进展...
- 入职半年小结 | 给应届校招算法同学的几点建议
- android 底部加载更多,android:ScrollView滑动到底部显示加载更多(示例代码)
- C# 图像编程 (1) 准备工作; 你好,空姐; 为空姐照片添加特效
- php curl CURLOPT_TIMEOUT_MS 小于1秒 解决方案
- java qlv转mp4 代码_怎么将qlv格式转换成mp4?教你快速转换视频格式的技巧
- DLL劫持技术解析(DLL Hijack)
- application/json和application/x-www-form-urlencoded使用选择
- 【Vue基础】前端工程化Vue项目
- 3dmax骨骼的绑定
- 云服务器存储文件,云服务器存储文件
- 【PYTHON数据分析实战】电影票房数据分析(一)数据采集
- 计算机毕业设计ssm陈氏商城9pd36系统+程序+源码+lw+远程部署
- 【Window10】解决win10家庭中文版找不到组策略gpedit.msc修改不了C盘文件
热门文章
- windows10下,from skimage import morphology 报错的解决办法
- 7.Deep Interest Network for Click-Through Rate Prediction论文详解
- 沈阳农业大学计算机往年录取分数6,沈阳农业大学历年分数线 2021沈阳农业大学录取分数线...
- 计算机管理信息系统大作业,管理信息系统期末大作业
- [k8s] 第十章 DashBoard
- @AUTORELEASEPOOL
- instancesRespondToSelector与respondsToSelector的区别
- java.lang.NullPointerException空指针问题
- python实现文件搜索_python实现搜索指定目录下文件及文件内搜索指定关键词的方法...
- 超赞的贪吃蛇、吃豆人和数字华容道等童年小游戏1行Python代码就能玩