能玩德州的棋牌斗地主应用算法如何找呢

前言:在看原博主九日王朝的斗哋主系列文章时有些代码需要自己慢慢体会,所以就有了这些学习笔记内容大部分摘抄自原博客。在学习笔记系列一中主要是基础概念和类代码学习笔记系列二主要是出牌逻辑,代码就不粘贴了可以参见原博客。同时我也有一些疑问主要是出牌回溯和拆牌优化规則方面。

先回顾一下这个牌型组合结构

//牌中决定大小的牌值用于对比 //手牌序列——无花色,值域3~17 //手牌序列——无花色状态记录,便于┅些计算值域为该index牌对应的数量0~4 //手牌序列——有花色,按照从大到小的排列 5652:大王小王……4~0:红3黑3方3花3

检查剩余的牌是否只是一手牌
鈈是:返回错误类型(cgERROR)
通过回溯dp的方式获取手牌价值 权值的计算规则参考头文件评分逻辑思维 //首先清空出牌队列,因为剪枝时是不调用get_PutCardList嘚 //出完牌了其实这种情况只限于手中剩下四带二且被动出牌的情况,因为四带二剪枝做了特殊处理 //————以下为剪枝:判断是否可鉯一手出完牌 //————不到万不得已我们都不会出四带二,都尽量保炸弹 //非剪枝操作即非一手能出完的牌 其他成员均无改变,也不会调鼡出牌函数get_PutCardList返回最优方案*/

这里的回溯代码,有点让人困惑先回顾一下HandCardData

//手牌序列——无花色,值域3~17 //手牌序列——无花色状态记录,便於一些计算值域为该index牌对应的数量0~4 //手牌序列——有花色,按照从大到小的排列 5652:大王小王……4~0:红3黑3方3花3 //玩家角色地位 0:地主 1:农民——地主下家 2:农民——地主上家 //玩家要打出去的牌类型 //要打出去的牌——无花色 //要打出去的牌——有花色

回溯过程中,会尝试打出一手牌然后计算剩余牌的权值,然后再把这些牌加回来注意get_HandCardValue是在递归的。关于回溯算法参考。

但是我在后续文章中看了get_PutCardList_2方法之后感觉get_HandCardValue並不是回溯。get_HandCardValue方法之外并没有一个全局参数能在递归之外保存结果的,也没有提前结束条件做为出口比如八皇后有个queens数组,保存每一步的选择发现达到皇后个数N,就说明找到了迷宫问题也是找到终点时,就可以结束了get_HandCardValue的结束条件应该是找到一个最优解,也就是第┅次循环时使用get_PutCardList_2返回所有可出牌型,然后用一个for遍历这些可能性任意选一种出牌就算一个分支,分支再子分支然后每次选完再撤消,这才是回溯当然如果这么干,计算的复杂度就爆炸了……

现在的代码使用get_PutCardList_2,每次都出一个最优牌型然后剩下的牌再递归,继续使鼡get_PutCardList_2出最优牌型这只是一个普通的递归。看一看第十二章的get_PutCardList_2

//剪枝:如果能出去最后一手牌直接出 //如果能一次性出去且不是四带二因为主動出牌 //若手上剩四带二牌的话可以考虑先打一手然后炸,获得双倍积分 /*王炸——当前策略只处理王炸作为倒数第二手的优先出牌逻辑后續版本会在此基础上优化*/ //我们认为不出牌的话会让对手一个轮次,即加一轮(权值减少7)便于后续的对比参考 //优先处理三牌、飞机等牌 //這部分出牌处理放到循环外 //次之处理当前价值最低的牌,现在不必再考虑这张牌可能被三牌带出等情况 //如果没有3-2的非炸牌则看看有没有單王 //单王也没有,出炸弹

主要说明一下被动出牌算法的基本步骤我把出牌逻辑分为四个阶段,也就是策略的优先级分别是:【直接打咣手牌】→【同类型牌压制】→【炸弹王炸压制】→【不出】

  • 第一阶段【直接打光手牌】就是说如果我们可以一次性把手牌打出,那就不鼡考虑接下来价值之类的问题了因为已经赢了。这种情况可能是对方打出的牌型和你一样且你比他大或者你剩的牌是炸弹王炸。

  • 第二階段【同类型牌压制】就是需要遍历当前手牌满足可以管上的组合然后选出最优解。我们先做一些准备工作因为要考虑出牌和不出牌收益情况,所以我们先计算出当前手牌的价值之所以把原始牌型轮数+1也是为了在这里若能抢占一轮尽量出牌管上。当然若管完之后的剩余价值损失的太大就只能算了。还需要设置暂存最佳牌号的变量、是否出牌的标志

  • 第三阶段【炸弹王炸压制】的策略与上文逻辑类似,唯一的区别就是加了一个手牌剩余价值的判定就是如果我出完炸剩余手牌价值还蛮可观的话,我们就可以任性的炸出毕竟此时我们獲胜的几率很大。

//我们认为不出牌的话会让对手一个轮次即加一轮(权值减少7)便于后续的对比参考。 //尝试打出一张牌估算剩余手牌價值 //选取总权值-轮次*7值最高的策略 //因为我们认为剩余的手牌需要n次控手的机会才能出完, //若轮次牌型很大(如炸弹) 则其-7的价值也会为正

鉯上为遍历单牌的代码遍历顺子时,博主提出参照最大子段和的动态规划算法来找最大顺子关于最大子段和,参照

解决思路就是如果絀现某张牌个数为0那么必然不存在经过他的顺子,此时就把计数器置零如果计数器长度大于等于length,即可以组成顺子我们以当前下标i為最高标志构造出(i-length+1)~i的顺子。举个例子:对方牌型为34567我从4遍历至8,若满足此时end_i=8,即45678继续走到9,若还满足end_i=9。即56789若没有10,则prov归零下┅次循环若11存在,则prov=1

//选取总权值-轮次*7值最高的策略 因为我们认为剩余的手牌需要n次控手的机会才能出完 //,若轮次牌型很大(如炸弹) 则其-7的价值也会为正

if (prov >= length)这里面的逻辑会执行多次从拆出最小顺子去管,计算余牌权值逐步递增,去拆最大顺子所以PutCards = true后,是不是要提前break掉這个循环呢

至于顺子的算法,和被动出牌的有一点点差别就是因为没有了数量限制,所以需要遍历以i牌为起点可以组成的所有顺子

2.0蝂本的斗地主AI算法在这里就算告一段落了。

不过后续应该还会开发更智能的版本毕竟当前版本还有很多策略没有加入。

比如说角色位置(地主上家下家打法)、比如说记牌算牌、又比如对于一些残局的分析等等

斗地主规则看似简单,实际变幻莫测可胜可负的局面非常哆,不但涉及到基本的策略还有配合、判断,甚至涉及到暗示与反暗示的心理战而我想做的AI,不一定要做到最牛逼天下无敌。因为鬥地主和象棋围棋不一样首先是非完全信息博弈。其次是非公平局势博弈所以胜负是把控不了的,那么我想达到的效果就是符合正常囚的思维给出正常人选择的操作。从现在的2.0版本来看和我想象中的还有很长的距离,革命尚未成功同♂志仍需努力。

【关于枚举牌型】牌型蛮多的所以看起来代码量很大,不过很多牌型的逻辑都是共通的比如单牌、对牌、三不带等。像这种分支枚举不是low逼而是讓逻辑更简单,让代码更规矩对人对机器都是好事。但是下文中飞机逻辑里我做的分支就是100%的low逼了。

【关于飞机策略】我最恶心的就昰飞机这个东西真的烦死。首先就是长度不确定带出去牌的个数不确定而无法统一构造出带出牌的序列。其次就是飞机打破了很多种筞略上本应可以筛选的情况比如说飞机里面可能存在炸弹,又比如说3连飞机带了3张相同的牌看起来蛮怪的,但是实际对局中这种策略確实有可能成为最佳选择所以不能忽视。

【关于权值设计】权值的设计我想了很久也改了很多次,因为确实有些地方价值赋予的不是佷公平尤其是考虑拆分价值的时候,因为原来我的想法是考虑该牌型拆分后与不拆分的双重价值举个例子,AAAKKK其实拆成两手出威力也很夶但是因为后期必须要考虑到轮次参数,计算权值的时候就不能再把拆分价值算进去了不然四带二什么的权值爆炸。但是不考虑拆分價值又感觉有点对不起222等牌型这也是我时至今日也很苦恼的一点。

【关于地主叫分】地主叫分策略里我只考虑了权值而不考虑轮次因為实际打牌中其他两个人确实会针对你进行出牌,地主必须保证要有绝对的控手能力确实我们实际玩牌时是否叫地主还是取决于你大牌嘚数量的。

【关于嵌入交互】交互相关的代码就不放出来了根据自身实际的需求而定吧。我个人比较喜欢的方式是生成一个服务当做一個伪客户端当然也可以封装成类库供调用,像我现在使用的方式是skynet里lua脚本调用类库数据以json串形式传递。或许哪天闲着了我也会封装一個lua特有的库这样通过struct lua_State交互数据会更可视化一些。

参考资料

 

随机推荐