其实这个不仅是麻将,是因为某论坛活动需要所以做一个类似于麻将的游戏,和牌算法都是差不多的。
只不过牌改成4种数字(也是1-9各4张)以及东南西北(没有白发中了)
然后说一下这边的情况,现在最大的问题是:当Player摸到一张牌,然后显示出是否听牌的标志(用官话说就是是否能立直,用通俗的话说就是,摸到牌之后那个“听”字是否会亮起来)。
麻将的和牌程序其实不难写,用递归就可以,咱编写和牌程序最长也不超过3毫秒。
但是听牌的程序就稍微有点麻烦了,听牌的意思是,只要再摸一张牌,使该牌组构成和牌,就是听牌了。
而且实际应用中经常不仅仅判定一副牌是否听牌,而且还要输出所有待和之牌。(比如,2、2、2、3则输出1、3、4)
这样的话要把麻将库里的每一张牌(1,3,5,7以及11-19,21-29,31-39,41-49共40种)都要遍历一遍才可以所以耗时基本上等于3毫秒乘以40(当然也是最坏的情况),这就是120毫秒了。
然后接下来是更麻烦的事情,Player摸到一张牌后,再打出一张牌是否能听牌,牌组里一般是有14张的(算摸来的牌),这样还得把牌组里的牌都遍历一下,把每一张牌都考虑成打出的牌,然后检查牌组打出该牌后是否听牌。这样,又要把上述所需时间再乘以14,也就是1680毫秒,这样……就相当不能接受了。谁也不想摸完一张牌然后等上1.68秒后才有反应(毕竟每次摸牌后都要检测听牌,即使没听也得检测)。所以带着这个头疼的事实我就设法对算法进行改进。
首先我一开始的听牌程序是根据是否和牌和遍历牌库来进行设计的,一般来说听牌时待和牌至少是听牌牌组里的某张牌或者某张牌相邻的牌,所以我把不相邻的牌直接排除在外。这样算法能快一些,但是实际上没块多少,而且还不会影响到最坏的情况(尤其类似于九莲宝灯、四连刻这类清一色的牌型),所以基本没啥改进。
后来我就不在考虑从和牌方式入手了,我直接重新按照递归的方法去写检验一副牌是否听牌的程序,检测方式就跟代码里的注释那样了。不过听牌检测我没做改动,还是将手里的牌都遍历了一遍。这样我使用最坏情况的牌型测出来的时间大概是798毫秒。虽然短了很多但是还是不可接受。
所以问题来了……咱写的代码都很简单啦,希望大神们能帮忙看看哪里还有改进的建议。暂且不考虑国士无双和七对子这种特殊牌型。
另外我自己最后的2个想法,虽然有点累:
1、我想把所有清一色的牌型都直接存成数据(即听牌表),然后听牌时直接查。这样也不需要什么递归了……但是写数据工作量还很大,而且不排除有漏掉的情况。
2、就像处理检测一副牌组是否听牌的程序那样,再重新写检测一副牌打出某张牌之后是否进入听牌的程序,重新用递归写。既然用和牌程序来写听牌检测程序是个很糟糕的做法,那么用听牌检测程序来写检测打出某张牌后是否听牌的程序同样是个很糟糕的做法。不过据我初步试验,起始值太不好写了……
//牌号定义
//红:11-19
//绿:21-29
//黄:31-39
//紫:41-49
//东:1 南:3 西:5 北:7
//未定义:0,2,4,6,8,9,10,20,30,40,50及以上
function test(f:Function, n:Number) {
//检验目标函数运算时间
var t = getTimer();
for (var i = 0; i<=n; i++) {
f();
}
trace(((getTimer()-t))/n+"ms");
}
function copy(deck:Array):Array {
//复制牌组
var copied = [];
for (var i = 0; i<deck.length; i++) {
copied[i] = deck[i];
}
return copied;
}
function copyUnique(deck:Array):Array {
//复制牌组,并且删除冗余牌
var copied = [];
var j = 0;
for (var i = 0; i<deck.length; i++) {
if (deck[i] != copied[j-1]) {
copied[j] = deck[i];
j++;
}
}
return copied;
}
function copyExtend(deck:Array):Array {
//复制牌组,删除冗余牌并向周围扩散1个数字
var copied = [];
for (var i = 0; i<deck.length; i++) {
copied.push(deck[i]);
if (deck[i]+1>10 && (deck[i]+1)%10 != 0) {
copied.push(deck[i]+1);
}
if (deck[i]-1>10 && (deck[i]-1)%10 != 0) {
copied.push(deck[i]-1);
}
}
copied.sort();
return copyUnique(copied);
}
function haveTile(deck:Array, tile:Number):Boolean {
//判断牌组里是否有目标牌
for (var i = 0; i<deck.length; i++) {
if (tile == deck[i]) {
return true;
}
}
return false;
}
function countTile(deck:Array, tile:Number):Number {
//计算牌组里目标牌的数目
var count = 0;
for (var i = 0; i<deck.length; i++) {
if (tile == deck[i]) {
count++;
}
}
return count;
}
function isShun(deck:Array):Boolean {
//判断长度为3的牌组是否成顺
if (deck.length != 3) {
return false;
}
deck.sort();
return (deck[2] == deck[1]+1) && (deck[1] == deck[0]+1);
}
function isKe(deck:Array):Boolean {
//判断长度为3的牌组是否成刻
if (deck.length != 3) {
return false;
}
return (deck[2] == deck[1]) && (deck[1] == deck[0]);
}
function isDui(deck:Array):Boolean {
//判断长度为2的牌组是否成对
if (deck.length != 2) {
return false;
}
return deck[1] == deck[0];
}
function isKang(deck:Array):Boolean {
//判断长度为4的牌组是否成杠
if (deck.length != 4) {
return false;
}
return (deck[3] == deck[2]) && (deck[2] == deck[1]) && (deck[1] == deck[0]);
}
function isKan(deck:Array):Boolean {
//判断长度为2的牌组是否为砍张待和
if (deck.length != 2) {
return false;
}
deck.sort();
return (deck[1] == deck[0]+2) && (deck[0]%10 != 9) && (deck[0]>10);
}
function isLiangMian(deck:Array):Boolean {
//判断长度为2的牌组是否为两面待和
if (deck.length != 2) {
return false;
}
deck.sort();
return (deck[1] == deck[0]+1) && (deck[0]%10 != 1) && (deck[1]%10 != 9);
}
function isShangBian(deck:Array):Boolean {
//判断长度为2的牌组是否为上边张待和(8、9,和7)
if (deck.length != 2) {
return false;
}
deck.sort();
return (deck[1] == deck[0]+1) && (deck[1]%10 == 9);
}
function isXiaBian(deck:Array):Boolean {
//判断长度为2的牌组是否为下边张待和(2、1,和3)
if (deck.length != 2) {
return false;
}
deck.sort();
return (deck[1] == deck[0]+1) && (deck[0]%10 == 1);
}
function isHu(deck:Array):Boolean {
//判断牌组(长度除以3余2)是否组成和牌
//已知最长耗时2.7459ms
if (deck.length%3 != 2) {
return false;
}
if (deck.length == 2) {
if (isDui(deck)) {
return true;
} else {
return false;
}
}
var extract_ke = cutKeEnum(deck);
for (var i = 0; i<extract_ke.count; i++) {
if (isHu(extract_ke.remain[i])) {
return true;
}
}
var extract_shun = cutShunEnum(deck);
for (var i = 0; i<extract_shun.count; i++) {
if (isHu(extract_shun.remain[i])) {
return true;
}
}
return false;
}
//听牌算法
function ting(deck:Array):Array {
//判断牌组(长度除以3余1)是否听牌并输出所有待和之牌。
//遍历牌库里每一张牌,加入到牌组来判定是否和牌。
//已知最长耗时1751ms(太长了……)
var tingArray = [];
var tileArray = [11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 32, 33, 34, 35, 36, 37, 38, 39, 41, 42, 43, 44, 45, 46, 47, 48, 49, 1, 3, 5, 7];
for (var i = 0; i<tileArray.length; i++) {
var m_deck = copy(deck);
m_deck.push(tileArray[i]);
if (isHu(m_deck)) {
tingArray.push(tileArray[i]);
}
}
return tingArray;
}
function ting2(deck:Array):Array {
//判断牌组(长度除以3余1)是否听牌并输出所有待和之牌。
//基于ting1算法,选择牌库中与牌堆相同或者差1的牌来进行判别,其他一律不判别
var tingArray = [];
var tileArray = copyExtend(deck);
for (var i = 0; i<tileArray.length; i++) {
var m_deck = copy(deck);
m_deck.push(tileArray[i]);
if (isHu(m_deck)) {
tingArray.push(tileArray[i]);
}
}
return tingArray;
}
function ting3(deck:Array):Array {
//判断牌组(长度除以3余1或2)是否听牌并输出所有待和之牌。
//不通过判定和牌直接来判定是否听牌。
//单骑、一对、两面待、上边张、下边张、砍张则可以直接判定;
//长度为4的牌组进行对提取、刻提取和顺提取,然后将剩余牌型判定;
//长度大于4的牌组进行刻提取和顺提取,然后剩余牌型判定。
//已知最长耗时89ms(还是很长,不能接受,原因见后)
var tingArray = [];
if (deck.length%3 == 0) {
return [];
}
if (deck.length == 1) {
return [deck[0]];
}
if (deck.length == 2) {
if (isDui(deck)) {
return [deck[0]];
}
if (isKan(deck)) {
return [deck[0]+1];
}
if (isLiangMian(deck)) {
return [deck[0]-1, deck[1]+1];
}
if (isShangBian(deck)) {
return [deck[0]-1];
}
if (isXiaBian(deck)) {
return [deck[1]+1];
}
}
if (deck.length == 4) {
var extract_dui = cutDuiEnum(deck);
for (var i = 0; i<extract_dui.count; i++) {
tingArray=tingArray.concat(ting3(extract_dui.remain[i]));
}
var extract_ke = cutKeEnum(deck);
for (var i = 0; i<extract_ke.count; i++) {
tingArray=tingArray.concat(ting3(extract_ke.remain[i]));
}
var extract_shun = cutShunEnum(deck);
for (var i = 0; i<extract_shun.count; i++) {
tingArray=tingArray.concat(ting3(extract_shun.remain[i]));
}
}
if (deck.length>4) {
var extract_ke = cutKeEnum(deck);
for (var i = 0; i<extract_ke.count; i++) {
tingArray=tingArray.concat(ting3(extract_ke.remain[i]));
}
var extract_shun = cutShunEnum(deck);
for (var i = 0; i<extract_shun.count; i++) {
tingArray=tingArray.concat(ting3(extract_shun.remain[i]));
}
}
tingArray.sort();
return copyUnique(tingArray);
}
//提取操作
function cutTile(deck:Array, tile:Number):Array {
//从牌组中尝试提取目标牌(如果存在多个则只提取1个,如果不存在则不提取,返回剩下的牌组成的牌组)
var cut = copy(deck);
for (var i = 0; i<=cut.length-1; i++) {
if (cut[i] == tile) {
cut.splice(i, 1);
break;
}
}
return cut;
}
function cutDuiEnum(deck:Array):Object {
//对提取操作。
//返回一个对象:
//count为成功进行对提取操作的次数;
//extract[n]为一次对提取操作中提取的牌构成的牌组;
//remain[n]为一次对提取操作中剩余的牌构成的牌组;
//n的取值范围为0到count-1。
deck.sort();
var enum = new Object();
var uniqueDeck = copyUnique(deck);
enum.count = 0;
enum.extract = [];
enum.remain = [];
for (var i = 0; i<uniqueDeck.length; i++) {
var tile = uniqueDeck[i];
var cut = copy(deck);
if (countTile(cut, tile)>=2) {
cut = cutTile(cut, tile);
cut = cutTile(cut, tile);
enum.extract[enum.count] = [tile, tile];
enum.remain[enum.count] = copy(cut);
enum.count++;
}
}
return enum;
}
function cutShunEnum(deck:Array):Object {
//顺提取操作。
//返回一个对象:
//count为成功进行顺提取操作的次数;
//extract[n]为一次顺提取操作中提取的牌构成的牌组;
//remain[n]为一次顺提取操作中剩余的牌构成的牌组;
//n的取值范围为0到count-1。
//最长耗时950微秒。
deck.sort();
var enum = new Object();
var uniqueDeck = copyUnique(deck);
enum.count = 0;
enum.extract = [];
enum.remain = [];
for (var i = 0; i<uniqueDeck.length-2; i++) {
var tile = uniqueDeck[i];
var cut = copy(deck);
if (haveTile(cut, tile+1) && haveTile(cut, tile+2)) {
cut = cutTile(cut, tile);
cut = cutTile(cut, tile+1);
cut = cutTile(cut, tile+2);
enum.extract[enum.count] = [tile, tile+1, tile+2];
enum.remain[enum.count] = copy(cut);
enum.count++;
}
}
return enum;
}
function cutKeEnum(deck:Array):Object {
//刻提取操作。
//返回一个对象:
//count为成功进行刻提取操作的次数;
//extract[n]为一次刻提取操作中提取的牌构成的牌组;
//remain[n]为一次刻提取操作中剩余的牌构成的牌组;
//n的取值范围为0到count-1。
//最长耗时581微秒。
deck.sort();
var enum = new Object();
var uniqueDeck = copyUnique(deck);
enum.count = 0;
enum.extract = [];
enum.remain = [];
for (var i = 0; i<uniqueDeck.length; i++) {
var tile = uniqueDeck[i];
var cut = copy(deck);
if (countTile(cut, tile)>=3) {
cut = cutTile(cut, tile);
cut = cutTile(cut, tile);
cut = cutTile(cut, tile);
enum.extract[enum.count] = [tile, tile, tile];
enum.remain[enum.count] = copy(cut);
enum.count++;
}
}
return enum;
}
/////////////////////
function canLi(deck:Array, tile:Number):Array {
//判断抓到目标牌时,牌组能否打出一张牌进行立直,输出所有可以打出的牌。
//目前最短耗时798ms,使用ting3算法。
var li_array = [];
var m_deck = copy(deck);
m_deck.push(tile);
for (var i = 0; i<m_deck.length; i++) {
var m_deck2 = copy(m_deck);
m_deck2.splice(i, 1);
var m_ting = ting3(m_deck2);
trace("cut "+m_deck[i]+": "+m_ting);
if (m_ting.length != 0) {
li_array.push(m_deck[i]);
}
}
return li_array;
}
//Debug
function dispCut(n:Object) {
//Debug用:输出提取对象的内容。
trace("Enum count:"+n.count+"\n");
for (var i = 0; i<n.count; i++) {
trace(i+" Extract: "+n.extract[i].toString()+"\n");
trace(i+" Remain: "+n.remain[i].toString()+"\n");
}
}
------解决思路----------------------
如果合理利用对称性,还会提高效率大约4倍。
------解决思路----------------------
引入牌类下标index(4).
利用对称性,对于当前局面先映射到抽象类压缩表示中。
其余雷同了。