node.js——麻将算法(七)简易版麻将出牌AI2.0

本文针对麻将AI出牌逻辑进行了优化,解决了无闲牌时拆组打牌及仅凭听牌数决策的问题,引入了考虑牌间关联性和剩余牌数量的策略。

*文本为上一篇博客http://blog.youkuaiyun.com/sm9sun/article/details/77898734的部分追加优化

 

上一篇博客已经实现了基本的出牌逻辑,大部分情况能够给出正确的策略选择,但经过了一些测试,仍发现了几个严重的问题:

问题一:当手牌无闲牌时,偶尔会将完整的一组牌拆开打出。例如:二万、四万、七万、八万、三筒、五筒、一条、二条、三条、九条、九条

可能会打出一条。

发生该问题的原因是当计算needhun时,打出任意一张牌返回的结果是一样的(打二万和一条所需要的混牌数都是一样)。并且三张牌组又有一万(19优先策略)。故两张牌组战胜了三张牌组。其实这种问题只是不仅仅是两牌和三牌的关系,更能延伸出当手上没有单一的闲牌且needhun值相等时,当面对拆牌的情况,AI该如何抉择。按照我们大部分打牌的逻辑,如果该牌和手上其他的牌可以关联,那么我们会尽量的先留着,因为这种牌扩展性极强。所以在needhun值相等的情况下,我们还要参考下当前手牌有多少张牌和这张牌有关系,这样不但可以解决上述的两张牌组战胜了三张牌组的问题,也可以优先时牌变的紧凑。因为紧凑的牌型容易演变出听口多的结构。

 

解决方案:将原有的is_nexus方法(判断是否为单一)改成计算该牌和手牌有关系的总数

 

[javascript]  view plain  copy
 
  1. //返回单排和手牌有关系的个数  
  2. exports.has_nexus = function (i, arr) {  
  3.   
  4.     if (i > 26) {  
  5.         return arr[i];  
  6.     }  
  7.     else if (i % 9 == 8) {  
  8.         return arr[i] + arr[i - 1] + arr[i - 2];  
  9.     }  
  10.     else if (i % 9 == 7) {  
  11.         return arr[i] + arr[i - 1] + arr[i - 2] + arr[i + 1];  
  12.     }  
  13.     else if (i % 9 == 0) {  
  14.         return arr[i] + arr[i + 1] + arr[i + 2];  
  15.     }  
  16.     else if (i % 9 == 1) {  
  17.         return arr[i] + arr[i + 1] + arr[i + 2] + arr[i - 1];  
  18.     }  
  19.     else {  
  20.         return arr[i] + arr[i + 1] + arr[i + 2] + arr[i - 1] + arr[i - 2];  
  21.     }  
  22. }  


当needhun数相等时,可以优先挑出关系数最少的打出

 

 

[javascript]  view plain  copy
 
  1. else if (needhun == ret_needhun)  
  2.             {  
  3.                 if (nexus < ret_nexus) {  
  4.                     ret_nexus = nexus;  
  5.                     ret_pai = list[k];  
  6.                 }  
  7.                 else if (nexus == ret_nexus) {  
  8.                     if (list[k] > 26)//风牌优先打  
  9.                     {  
  10.                         ret_pai = list[k];  
  11.                     }  
  12.                     if ((list[k] % 9 < 1 || list[k] % 9 > 7) && ret_pai <= 26)//边牌优先打  
  13.                     {  
  14.                         ret_pai = list[k];  
  15.                     }  
  16.                 }                   
  17.             }  


由于这种策略是在保证needhun相等情况才可以的,所以整个函数判断结构由原来的先判断是否单一再判断所需赖子数改成先判断所需赖子数再考虑关系数。可见后续完整代码。

 

 

 

问题二:只考虑听牌数并非最佳策略甚至造成死听

这个问题上篇博客已经说了,当可以听牌时我们可以选择听口或者剩余牌多等不同的策略,若选择听口多则会造成报个死听的可能。

故新增选择听牌剩余牌最多的逻辑:

 

[javascript]  view plain  copy
 
  1. exports.GetTingPaiCount = function(Tinglist, holds, game_RemainMap)  
  2. {  
  3.     var RemainMap = [];  
  4.     if (game_RemainMap == null)//若参数为空,即无视出牌情况只考虑自身手牌,默认都为4个计算  
  5.     {         
  6.         for (var i = 0; i < 34; i++)  
  7.         {  
  8.             RemainMap[i] = 4;  
  9.         }  
  10.     }  
  11.     else  
  12.     {  
  13.         RemainMap = game_RemainMap.concat();  
  14.     }  
  15.   
  16.     for (var i = 0; i < holds.length;i++)  
  17.     {  
  18.         RemainMap[holds[i]]--;  
  19.     }  
  20.   
  21.     var TingPaiCount = 0;  
  22.     for (var i = 0; i < Tinglist.length;i++)  
  23.     {  
  24.         TingPaiCount += RemainMap[Tinglist[i]];  
  25.     }  
  26.   
  27.     return TingPaiCount;  
  28.   
  29. }  

 

 

至于维护这个RemainMap数组也很简单,游戏开始时为4,出牌时-1,碰牌时-2,吃牌时内两张牌分别-1,杠牌置0即可。

将原来的Tinglist比对改为TingPaiCount比对即可

 

 

[javascript]  view plain  copy
 
  1. var TingPaiCount = exports.GetTingPaiCount(Tinglist, list, RemainMap);  
  2.         if (TingPaiCount > 0)//至少有得胡 如果胡的牌都没了就换牌吧  
  3.         {  
  4.             //听牌数比对,也可以按其他方式比对,比如所听的牌接下来的剩余牌  
  5.             if (ret_tingpaicount < TingPaiCount) {  
  6.                 ret_tingpaicount = TingPaiCount;  
  7.                 ret_needhun = 1;  
  8.                 ret_pai = list[k];  
  9.             }  
  10.         }  
  11.         else if (ret_tingpaicount == 0)  



 

 

上个版本的一些BUG(前篇博客已经改正):

1.偶尔出现第一张牌总是优先打出的BUG

一开始发现这个BUG时我是懵逼的,经过了好久才找到问题。其实这是一个语法上的BUG,之前调用get_needhun_for_hu时为了把赖子先摘出来,将其置为了0,由于JS数组传递是引用,导致了后面按赖子为0算了,这样当手牌有赖子时,后面的返回结果都大于正确值。

解决方法:运算结束后还原数组,或用新数组。

 

2.出牌函数循环计算ret_needhun不正确

初始化最大值过小,我们计算needhun时一张废牌是会需要2张混牌的 故0xf(16)在极端的情况下并不满足最大值(东南西北中发白各一个就是14张了)。

解决方法:初始化最大值0x1a(26)

 

修改后的出牌方法完整代码:

 

[javascript]  view plain  copy
 
  1. exports.GetRobotChupai = function (list, special, hun, RemainMap) {  
  2.   
  3.     if (hun == null) {  
  4.         hun = -1;  
  5.     }  
  6.     var arr = [];  
  7.     var Tingobj = [];  
  8.   
  9.     for (var i = 0; i < special.mj_count; i++) {  
  10.         arr[i] = 0;  
  11.     }  
  12.   
  13.     for (var j = 0; j < list.length; j++) {  
  14.         Tingobj[j] = {};  
  15.         if (arr[list[j]] == null) {  
  16.             arr[list[j]] = 1;  
  17.         }  
  18.         else {  
  19.             arr[list[j]]++;  
  20.         }  
  21.     }  
  22.   
  23.     var ret_needhun = 0x1a;  
  24.     var ret_pai = list[0];  
  25.     var ret_tinglist = [];  
  26.     var ret_nexus = 0xff;  
  27.     var ret_tingpaicount = 0;  
  28.   
  29.   
  30.   
  31.     for (var k = 0; k < list.length; k++) {  
  32.   
  33.         if (list[k] == hun)  
  34.         {  
  35.             continue;  
  36.         }  
  37.         arr[list[k]]--;  
  38.         var Tinglist = new Array();  
  39.         var canting = exports.CanTingPai(arr, hun, Tinglist, special);  
  40.         var TingPaiCount = exports.GetTingPaiCount(Tinglist, list, RemainMap);  
  41.         if (TingPaiCount > 0)//至少有得胡 如果胡的牌都没了就换牌吧  
  42.         {  
  43.             //听牌数比对,也可以按其他方式比对,比如所听的牌接下来的剩余牌  
  44.             if (ret_tingpaicount < TingPaiCount) {  
  45.                 ret_tingpaicount = TingPaiCount;  
  46.                 ret_needhun = 1;  
  47.                 ret_pai = list[k];  
  48.             }  
  49.         }  
  50.         else if (ret_tingpaicount == 0) {  
  51.             var needhun = get_needhun_for_hu(arr, hun, special);  
  52.             var nexus = exports.has_nexus(list[k], arr);  
  53.             if (needhun < ret_needhun)//判断此张牌需要的混牌数  
  54.             {  
  55.                 ret_needhun = needhun;  
  56.                 ret_pai = list[k];  
  57.                 ret_nexus = nexus;  
  58.             }  
  59.             else if (needhun == ret_needhun)  
  60.             {  
  61.                 if (nexus < ret_nexus) {  
  62.                     ret_nexus = nexus;  
  63.                     ret_pai = list[k];  
  64.                 }  
  65.                 else if (nexus == ret_nexus) {  
  66.                            if (list[k] > 26)//风牌优先打  
  67.                     {  
  68.                         ret_pai = list[k];  
  69.                     }  
  70.                     else if (list[k] % 9 < 1 || list[k] % 9 > 7)//边牌优先打  
  71.                     {  
  72.                         ret_pai = list[k];  
  73.                     }  
  74.                     else if (arr[list[k] + 1] == 0 && arr[list[k] - 1]==0)//主要针对夹,优先拆  
  75.                     {  
  76.                         ret_pai = list[k];  
  77.                     }  
  78.                 }                   
  79.             }  
  80.   
  81.         }  
  82.         arr[list[k]]++;  
  83.   
  84.     }  
  85.     return ret_pai;  
  86. }  



 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值