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

相关文章

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁

ModelMapper基本使用和常见场景示例详解

《ModelMapper基本使用和常见场景示例详解》ModelMapper是Java对象映射库,支持自动映射、自定义规则、集合转换及高级配置(如匹配策略、转换器),可集成SpringBoot,减少样板... 目录1. 添加依赖2. 基本用法示例:简单对象映射3. 自定义映射规则4. 集合映射5. 高级配置匹

SQL BETWEEN 语句的基本用法详解

《SQLBETWEEN语句的基本用法详解》SQLBETWEEN语句是一个用于在SQL查询中指定查询条件的重要工具,它允许用户指定一个范围,用于筛选符合特定条件的记录,本文将详细介绍BETWEEN语... 目录概述BETWEEN 语句的基本用法BETWEEN 语句的示例示例 1:查询年龄在 20 到 30 岁

mysql中insert into的基本用法和一些示例

《mysql中insertinto的基本用法和一些示例》INSERTINTO用于向MySQL表插入新行,支持单行/多行及部分列插入,下面给大家介绍mysql中insertinto的基本用法和一些示例... 目录基本语法插入单行数据插入多行数据插入部分列的数据插入默认值注意事项在mysql中,INSERT I

mapstruct中的@Mapper注解的基本用法

《mapstruct中的@Mapper注解的基本用法》在MapStruct中,@Mapper注解是核心注解之一,用于标记一个接口或抽象类为MapStruct的映射器(Mapper),本文给大家介绍ma... 目录1. 基本用法2. 常用属性3. 高级用法4. 注意事项5. 总结6. 编译异常处理在MapSt

MyBatis ResultMap 的基本用法示例详解

《MyBatisResultMap的基本用法示例详解》在MyBatis中,resultMap用于定义数据库查询结果到Java对象属性的映射关系,本文给大家介绍MyBatisResultMap的基本... 目录MyBATis 中的 resultMap1. resultMap 的基本语法2. 简单的 resul

Java 枚举的基本使用方法及实际使用场景

《Java枚举的基本使用方法及实际使用场景》枚举是Java中一种特殊的类,用于定义一组固定的常量,枚举类型提供了更好的类型安全性和可读性,适用于需要定义一组有限且固定的值的场景,本文给大家介绍Jav... 目录一、什么是枚举?二、枚举的基本使用方法定义枚举三、实际使用场景代替常量状态机四、更多用法1.实现接

git stash命令基本用法详解

《gitstash命令基本用法详解》gitstash是Git中一个非常有用的命令,它可以临时保存当前工作区的修改,让你可以切换到其他分支或者处理其他任务,而不需要提交这些还未完成的修改,这篇文章主要... 目录一、基本用法1. 保存当前修改(包括暂存区和工作区的内容)2. 查看保存了哪些 stash3. 恢

使用Python获取JS加载的数据的多种实现方法

《使用Python获取JS加载的数据的多种实现方法》在当今的互联网时代,网页数据的动态加载已经成为一种常见的技术手段,许多现代网站通过JavaScript(JS)动态加载内容,这使得传统的静态网页爬取... 目录引言一、动态 网页与js加载数据的原理二、python爬取JS加载数据的方法(一)分析网络请求1