感觉网上关于麻将的源码资源很少,一般这种算法都是用递归,把牌堆分解成若干子牌堆然后针对2-3张牌的情形给出一个出口。

和牌算法比较常见,毕竟只要是麻将编程都要用到,后面两种虽然普通的麻将编程用不到,但是要编写AI对策以及某些特殊规则(例如日本麻将)就有用了,尤其一向听的算法。

三种算法原理差不多,都是先分析牌数较少的情形,然后牌数较多的情形通过牌堆分解后,对提取剩余牌堆调用自身。

本源码使用C++,分MDeck和MDeckExtract类。这两个类定义如下:

typedef int MTile;
class MDeck;
class MDeckExtract;
class MDeck
{
public:MTile tile[200];int length;
};
class MDeckExtract
{
public:int methodCount;MDeck extracted[10];MDeck remained[10];
};

其中MDeck类实际上就是一个由MTile(int)组成的数组和牌堆长度,跟CArray类很像。

而MDeckExtract类则用于存储牌堆的提取结果。其中methodCount是提取方案的数目,extracted存储提取方案中可以提取的目标牌堆,remain则存储剩余牌堆。举个简单的例子:

例如一个牌堆为 3,3,4,5,6,若对其进行顺子提取操作,则MDeckExtract应该是这样的:

methodCount 为 2,即两种提取方案。

extracted[0]为3,4,5,remain[0]为3,6;extracted[1]为4,5,6,remain[1]为3,3。

接下来是提取方法(其中有的较为简单的方法的定义例如牌堆排序、创建不包含重复的牌的副本、牌堆连接、加入等方法就不帖进来了,很容易实现)的源码。

 
MDeckExtract MDeck::extractDuizi(){sortTile();int i = 0;MDeck dr = removeRepeatedTile();MDeck dt;MDeck de;MDeckExtract e;MTile t;for(i = 0;i<dr.length;i++){t = dr.tile[i];dt = (*this);de.length = 0;if(dt.countTile(t)>1){dt.removeTile(t);dt.removeTile(t);de.pushTile(t);de.pushTile(t);e.extracted[e.methodCount] = de;e.remained[e.methodCount] = dt;e.methodCount ++;}}return e;
}MDeckExtract MDeck::extractShunzi(){sortTile();int i = 0;MDeck dr = removeRepeatedTile();MDeck dt;MDeck de;MDeckExtract e;MTile t;for(i = 0;i<dr.length;i++){t = dr.tile[i];dt = (*this);de.length = 0;if(dt.countTile(t+1)>0 && dt.countTile(t+2)>0){dt.removeTile(t);dt.removeTile(t+1);dt.removeTile(t+2);de.pushTile(t);de.pushTile(t+1);de.pushTile(t+2);e.extracted[e.methodCount] = de;e.remained[e.methodCount] = dt;e.methodCount ++;}}return e;
}MDeckExtract MDeck::extractKezi(){sortTile();int i = 0;MDeck dr = removeRepeatedTile();MDeck dt;MDeck de;MDeckExtract e;MTile t;for(i = 0;i<dr.length;i++){t = dr.tile[i];dt = (*this);de.length = 0;if(dt.countTile(t)>2){dt.removeTile(t);dt.removeTile(t);dt.removeTile(t);de.pushTile(t);de.pushTile(t);de.pushTile(t);e.extracted[e.methodCount] = de;e.remained[e.methodCount] = dt;e.methodCount ++;}}return e;
}

接下来就是基于牌堆分解递归的和牌算法。大致步骤如下(这个大家似乎都经常用):

1. 检查牌堆长度是否被3除余2(因为和牌情况只能是2、5、8、11、14等张数的情形),若不是,返回false。

2. 检查牌堆是否满足特殊情况,例如七对子(有的规则里有国士无双等),若是,返回true,否则继续。

3. 检查牌堆长度是否为2,若是,则牌堆两张相同则返回true,否则返回false;若长度不为2,则继续。

4. 对牌堆进行顺子提取,然后对提取结果中的remain(Deck数组)里的每个元素(Deck)分别执行该算法,只要有一个元素(Deck)执行结果为true,则直接返回true,否则继续。

5. 对牌堆进行刻子提取,然后对提取结果中的remain(Deck数组)里的每个元素(Deck)分别执行该算法,只要有一个元素(Deck)执行结果为true,则直接返回true,否则继续。

6. 返回false。

代码:

bool MDeck::isHu(){MDeckExtract e;int i;//对特殊情况判别if(length == 14){e = extractDuizi();if(e.methodCount == 7){return true;}}//对一般情况判别if(length % 3 != 2){return false;}if(length == 2){if(isDuizi()){return true;}else{return false;}}e = extractKezi();for(i = 0;i<e.methodCount;i++){if(e.remained[i].isHu()){return true;}}e = extractShunzi();for(i = 0;i<e.methodCount;i++){if(e.remained[i].isHu()){return true;}}return false;
}

然后是基于牌堆分解递归的听牌算法。

所谓听牌,就是手里的牌再加一张就能构成一副和牌。

听牌算法其实可以使用和牌算法和对所有麻将牌的遍历来实现,但是这样极端影响速度,因此最好还是从本源做起。

听牌跟和牌不一样,因为听牌方式有3种,分别是摸一张构成对子(单骑)、刻子(对倒)、顺子(边张或者砍张)。而构成牌的数目也不同,分别是2、3、3,因此,听牌牌堆的长度应该满足2+3N-1或者3+3N-1即被3除余1或者2,因此被3整除的情形是肯定不能算听牌的。

该算法返回牌堆能听的所有牌,大致步骤如下:

1. 初始化返回结果,置为空牌堆。

2. 检查牌堆长度是否被3整除,若是,返回空牌堆,否则继续(如果不对这种情况进行处理,则自然而然就返回空牌堆了,所以这步可以去掉)。

3. 检查牌堆长度是否为13,若是,检查是否有6组对子,如果有,则听剩余的那张牌(七对子的规则),将其加入返回结果;不管有没有6组对子,都继续进行。

4. 检查牌堆长度是否为1,若是,将仅有的这张牌加入返回结果(单骑听牌),然后继续进行。

5. 检查牌堆长度是否为2,若是,分以下情况进行讨论:

a) 两张牌相同,将这张牌加入返回结果(双碰听牌),然后继续进行。

b) 两张牌相邻,将两张牌往两边扩展的那张牌(例如7,8则扩展的牌为6和9,若为8和9则只有7)均加入返回结果(边张听牌),然后继续进行。

c) 两张牌隔一,将这两张牌中间的那张牌(例如6,8则中间那张牌为7)加入返回结果(砍张听牌),然后继续进行。

d) 不满足以上三种情形,则不加入,并继续进行。

6. 检查牌堆长度是否为4,若是,依次对其进行对子提取,刻子提取和顺子提取,并将提取结果的remain数组里的每个元素(Deck)都做听牌算法,并将结果悉数加入返回结果,然后继续进行。

7. 检查牌堆长度是否大于4,若是,依次对其进行刻子提取和顺子提取(同6一样,只不过不做对子提取),并将提取结果的remain数组里的每个元素(Deck)都做听牌算法,并将结果悉数加入返回结果,然后继续进行。

8. 将这个返回结果的非重复牌堆副本作为返回值,OK。

代码:

MDeck MDeck::ting(){int i ;MDeckExtract e;MDeck dr;MDeck result;//特殊情况if(length == 13){dr = removeRepeatedTile();e = extractDuizi();if(e.methodCount == 6){for(i=0;i<dr.length;i++){if(countTile(dr.tile[i])==1){result.pushTile(dr.tile[i]); break;}}}}if(length == 1){result.pushTile(tile[0]);return result;}if(length == 2){if(isDuizi()){result.pushTile(tile[0]);}if(isShangbianzhang()){result.pushTile(tile[0]-1);}if(isXiabianzhang()){result.pushTile(tile[0]+2);}if(isKanzhang()){result.pushTile(tile[0]+1);}if(isLiangmian()){result.pushTile(tile[0]-1);result.pushTile(tile[0]+2);}return result;}if(length == 4){e = extractDuizi();for(i = 0;i<e.methodCount;i++){result.combineDeck(e.remained[i].ting());}e = extractKezi();for(i = 0;i<e.methodCount;i++){result.combineDeck(e.remained[i].ting());}e = extractShunzi();for(i = 0;i<e.methodCount;i++){result.combineDeck(e.remained[i].ting());}}if(length > 4){e = extractKezi();for(i = 0;i<e.methodCount;i++){result.combineDeck(e.remained[i].ting());}e = extractShunzi();for(i = 0;i<e.methodCount;i++){result.combineDeck(e.remained[i].ting());}}return result.removeRepeatedTile();
}

最后是基于牌堆分解递归的一向听算法。

所谓一向听,就是牌堆打出一张后进入听牌的状态。例如制作麻将游戏中,玩家摸到牌后,判断听牌按钮是否会亮起就需要该方法的支撑;日本麻将中的能否进行“立直”也需要该算法的支撑。

同听牌算法一样,一向听的算法可以根据听牌算法和对手牌的遍历来实现,但是该方法也是特别耗时……

如果从源头做起的话,一向听的分析方式要比听牌还要复杂……该方法返回所有打出的牌使其成为听牌状态的集合,具体步骤如下:

1. 初始化返回结果,置为空牌堆。

2. 检查牌堆是否已经构成和牌,若是,则返回牌堆本身,否则继续(这步可以省略,某些极端情况可以大量减少运行时间,毕竟玩麻将基本遇不到自己和牌后不宣告和牌反而报听的。如果是检测算法的运算效率,强烈建议不要添加这步)。

3. 检查牌堆长度是否被3除余1,若是,返回空牌堆,否则继续(如果不对这种情况进行处理,则自然而然就返回空牌堆了,所以这步可以去掉)。

4. 检查牌堆长度是否为14,若是,检查是否有7组对子,如果有,则将每一个对子的牌加入返回结果(即七对子已经和牌);若有6组对子,将剩余的两张落单的牌加入返回结果(也就是说,打出任何一张落单的牌都能进入七对子听牌状态),然后继续进行。

5. 检查牌堆长度是否为2,若是,将这两张牌加入返回结果,然后继续进行。

6. 检查牌堆长度是否为3,若是,则:若任意两张牌数值距离<=2,则将第三张牌加入返回结果,然后继续进行。

例如,这三张牌是2,3,5,由于2和3距离位1<2,因此将5加入返回结果;2和5的距离为3>2,PASS;3和5的距离为2,将2加入返回结果,总共就是将2和5加入返回结果。

7. 检查牌堆长度是否为5,若是,依次对其进行对子提取,刻子提取和顺子提取,并将提取结果的remain数组里的每个元素(Deck)都做一向听算法,并将结果悉数加入返回结果,然后继续进行。

8. 检查牌堆长度是否大于5,若是,依次对其进行刻子提取和顺子提取(同7一样,只不过不做对子提取),并将提取结果的remain数组里的每个元素(Deck)都做一向听算法,并将结果悉数加入返回结果,然后继续进行。

9. 将这个返回结果的非重复牌堆副本作为返回值,OK。

代码:

MDeck MDeck::xting(){int i ;MDeckExtract e;MDeck dr;MDeck result;//特殊情况if(length == 14){dr = removeRepeatedTile();e = extractDuizi();if(e.methodCount == 7){for(i=0;i<7;i++){result.pushTile(tile[i]);}}if(e.methodCount == 6){for(i=0;i<dr.length;i++){if(countTile(dr.tile[i]) % 2 ==1){result.pushTile(dr.tile[i]); }}}}if(length == 2){result.pushTile(tile[0]);result.pushTile(tile[1]);}if(length == 3){if (isDiffBelow2(tile[0], tile[1])) {result.pushTile(tile[2]);}if (isDiffBelow2(tile[0], tile[2])) {result.pushTile(tile[1]);}if (isDiffBelow2(tile[1], tile[2])) {result.pushTile(tile[0]);}}if(length == 5){e = extractDuizi();for(i = 0;i<e.methodCount;i++){result.combineDeck(e.remained[i].xting());}e = extractKezi();for(i = 0;i<e.methodCount;i++){result.combineDeck(e.remained[i].xting());}e = extractShunzi();for(i = 0;i<e.methodCount;i++){result.combineDeck(e.remained[i].xting());}}if(length > 5){e = extractKezi();for(i = 0;i<e.methodCount;i++){result.combineDeck(e.remained[i].xting());}e = extractShunzi();for(i = 0;i<e.methodCount;i++){result.combineDeck(e.remained[i].xting());}}return result.removeRepeatedTile();
}

麻将的和牌、听牌以及一向听(即能否打一张牌进行立直)的算法。相关推荐

  1. php 判断5张牌是不是顺子,从扑克牌中随机抽取5张牌,判断是不是一个顺子,即这5张牌是不是连续(面试题)...

    从扑克牌中随机抽取5张牌,判断是不是一个顺子,即这5张牌是不是连续的2-10位数字本身,A为1,J为11,Q为12,K为13,而大小王可以看成任意数字 统计数组中出现的次数,判断等于0,大于1大于2大 ...

  2. Java黑皮书课后题第7章:*7.29(游戏:挑选四张牌)编写一个程序,从一副52张牌中选出4张,然后计算它们的和。Ace King Quee Jack分别表示1、13、12和11,显示得到和24的次

    *7.29(游戏:挑选四张牌)编写一个程序,从一副52张牌中选出4张,然后计算它们的和.Ace King Quee Jack分别表示1.13.12和11,显示得到和为24的选牌次数 题目 题目描述 破 ...

  3. 扑克牌中的顺子。从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王可以看成任意数字。

    面试题36:扑克牌中的顺子 1.题目描述 题目:从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的.2~10为数字本身,A为1,J为11,Q为12,K为13,而大.小王可以看成任意数字 ...

  4. 有13张扑克牌,怎样确定牌中有没有顺子(顺子:连续的5张牌)

    有13张扑克牌,怎样确定牌中有没有顺子(顺子:连续的5张牌) 分析思路如下: 1.可能出现的牌为数字(2-10).(J-K,A,大小王),先将不能直接作为数字排列的牌替换成数字: 2.将整个数组转换为 ...

  5. C#:今日上机问题描述:从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2-10为数字本身,A为1,J为11,Q为12,K为13,而大小王可以看成任意数字。

    using System; //写的不太严格,直接用14张牌来抽了,1-13,大小王为0,没有判断相同的不能超过4张,为0的相同不能超过两张. namespace _201919144122WeiXu ...

  6. 简易的不科学立直麻将学习笔记(1)-进攻策略-门清编-简单的两面听向做牌指南...

    还没想好这部分要写什么,所以坑先挖上吧 由于最近打麻将恶调过多了,这个时候就该反思下自己水平是不是不行而不是发牌姬搞我 由于一些众所周知的原因,一直打麻将不会提高你的水平反而会让你越打越菜 所以这个系 ...

  7. 【转】判断五张牌是不是一个顺子

    转自:http://hi.baidu.com/erennetwork/blog/item/f2942435d834e650ad4b5f8a.html 从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5 ...

  8. 出手比王兴更狠!他靠最后一张牌收编饿了么,成为阿里“扫地僧”!

    ????????关注后回复 "进群" ,拉你进程序员交流群???????? 作者:电商君 来源:电商报(ID:kandianshang)中国电商影响力媒体! 突发! 俞永福取代王磊 ...

  9. 模拟扑克牌,随机抽取五张牌,判断是否为同花顺

    今天去面试,遇到这个面试题,当时想法想通了,我跟面试官说给我半小时,但是超时了没写出来. 回来复盘一下~ 思路步骤: ①创建map容器存储扑克牌key为牌的索引,value为牌的值 ②按照同花顺顺序存 ...

最新文章

  1. python每行输出30个字_python_30期【for循环】
  2. 【iVX 初级工程师培训教程 10篇文拿证】06 数据库及服务
  3. sqlserve 热备用状态更新_燃气地暖一个月费用多少钱?看完收藏备用
  4. fasta文件中DNA to RNA
  5. 自动驾驶——驾驶员反应时间的文献调研
  6. ssh-key生成密钥及SSH无密码登录的配置(转载)
  7. 两台服务器centos7.x 直接文件共享,文件挂载 nfs
  8. Android引领移动互联网革命的七大理由
  9. android游戏录音,音频录音剪辑软件 1.1.15 安卓版
  10. 【渝粤题库】陕西师范大学800012 区域经济学
  11. NOI Linux 2.0版发布
  12. matlab剪切板中内容清除,清除剪贴板的内容
  13. 计算机屏幕出现条纹w7,电脑重装win7后屏幕出现条纹怎么办
  14. python自动化交易_python 优矿自动化交易
  15. 开箱即用!使用Rancher 2.3 启用Istio初体验
  16. proftpd java_Proftpd文件系统安装
  17. 四川汶川县今天又连发生地震!
  18. antd Upload手动上传(react)
  19. Ubuntu编译Android系统源码(msm8909_android5.1.1)
  20. Android——调试之 Log和LogCat的详解

热门文章

  1. 海量GPS轨迹数据集
  2. 主动变被动9个例句_语法知识:被动语态怎么用,你知道吗?
  3. html拖动圆纽改变,绘制一个可拖动的圆圈,其中心信息使用easeljs动态更改 - html5代码 - 源码查...
  4. TaskMan(任务侠),进程监控小程序
  5. stm32学习记录——扫码开锁
  6. 算法期中考试题目+代码
  7. 博客作者简介--Dynamics 365技术到项目经理之路
  8. 树莓派系统配置-raspi-config
  9. 超级简单Vue实现手风琴布局
  10. VCD超时长刻录及设置