node.js——麻将算法(一)基本判胡

2024-01-10 06:18
文章标签 算法 js 基本 node 麻将 判胡

本文主要是介绍node.js——麻将算法(一)基本判胡,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

大家好,失踪已久的九日哥回来了   奋斗


由于前段时间一直专注于开发鉴黄,所以身心都有些不适,故也给了自己很长的放松时间~

然而回来了之后,九日哥毅然决然的选择了棋牌(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









这篇关于node.js——麻将算法(一)基本判胡的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/589828

相关文章

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

用js控制视频播放进度基本示例代码

《用js控制视频播放进度基本示例代码》写前端的时候,很多的时候是需要支持要网页视频播放的功能,下面这篇文章主要给大家介绍了关于用js控制视频播放进度的相关资料,文中通过代码介绍的非常详细,需要的朋友可... 目录前言html部分:JavaScript部分:注意:总结前言在javascript中控制视频播放

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

SpringBoot整合MybatisPlus的基本应用指南

《SpringBoot整合MybatisPlus的基本应用指南》MyBatis-Plus,简称MP,是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,下面小编就来和大家介绍一下... 目录一、MyBATisPlus简介二、SpringBoot整合MybatisPlus1、创建数据库和

nvm如何切换与管理node版本

《nvm如何切换与管理node版本》:本文主要介绍nvm如何切换与管理node版本问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录nvm切换与管理node版本nvm安装nvm常用命令总结nvm切换与管理node版本nvm适用于多项目同时开发,然后项目适配no

如何通过Golang的container/list实现LRU缓存算法

《如何通过Golang的container/list实现LRU缓存算法》文章介绍了Go语言中container/list包实现的双向链表,并探讨了如何使用链表实现LRU缓存,LRU缓存通过维护一个双向... 目录力扣:146. LRU 缓存主要结构 List 和 Element常用方法1. 初始化链表2.

Node.js net模块的使用示例

《Node.jsnet模块的使用示例》本文主要介绍了Node.jsnet模块的使用示例,net模块支持TCP通信,处理TCP连接和数据传输,具有一定的参考价值,感兴趣的可以了解一下... 目录简介引入 net 模块核心概念TCP (传输控制协议)Socket服务器TCP 服务器创建基本服务器服务器配置选项服

mac安装nvm(node.js)多版本管理实践步骤

《mac安装nvm(node.js)多版本管理实践步骤》:本文主要介绍mac安装nvm(node.js)多版本管理的相关资料,NVM是一个用于管理多个Node.js版本的命令行工具,它允许开发者在... 目录NVM功能简介MAC安装实践一、下载nvm二、安装nvm三、安装node.js总结NVM功能简介N

Python中多线程和多进程的基本用法详解

《Python中多线程和多进程的基本用法详解》这篇文章介绍了Python中多线程和多进程的相关知识,包括并发编程的优势,多线程和多进程的概念、适用场景、示例代码,线程池和进程池的使用,以及如何选择合适... 目录引言一、并发编程的主要优势二、python的多线程(Threading)1. 什么是多线程?2.