以斗地主AI为例,探讨数值体系的设计和后期调整方案(三)

发表于2017-08-11
评论2 4.6k浏览


上一篇,我们把牌型分定义之后,基本找出手上的牌最好的分类了。现在我们可以作出这样的策略,永远只按照最高分牌型打,这样的话我们基本可以以最少回合数把手牌出完。但是这样真的是像真人的打法吗?

查看第一篇:以斗地主AI为例,探讨数值体系的设计和后期调整方案(一)

查看第二篇:以斗地主AI为例,探讨数值体系的设计和后期调整方案(二)

明显不是,我们会有抉择,可以小亏但不让对手出牌那么舒服,甚至把宁愿自己的牌拆的千仓百孔,自己不可能赢了,完全靠队友,也要把对手卡住。这种自损1000杀敌100的行为,是光从分数上无法体现的。这时候,就需要我们一开始就定义的,之前没提到的tactics——出牌策略来决定我们的打法了。当然,也可以认为前面的判断分数等一些列工作,都是为了按策略出牌作准备的,不过如果分数的工作做好了,策略的选择也只是顺其自然的一步而已。

我个人定义了三种策略,全攻、均衡和全守。

全攻就是基本按照自己的牌型走,除非特殊情况可以小亏损跟牌

均衡就是可以接受亏损大一些,但目标还是自己胜利的方式。

全守就是以让对手前期难受为目标,破话对手的节奏。(这种方式基本打牌没想赢了)

用哪种方式是由牌的分数决定的,分高了靠自己,分低了靠盟友。地主只能是全攻类型的,遇到特殊牌型还需要额外添加处理策略。

我是以-100-300为分界线,区分三种策略的。

虽然通过了很多次调整了,只能说这个区分是可行的但并不满意的。

尤其是,我原本做为起始牌组做的策略区分,没有实行动态调整方案,而前期和后期的分界线是不尽相同的。只能作为一个可用的过渡区分吧。

 

现在我们看看API的四个函数:

 

// JiaoDiZhu 叫几分,回复3,即叫三分,回复0,即不叫

func JiaoDiZhu(pai []int32) int32

由于底牌的随机性,这个只要看手上大牌分值经行判断就好了,例如3—10的顺子看起来很爽,摸完底牌就难说了。

 

// JiaBei 要不要加倍

func JiaBei(pai []int32) bool

这一个对流程是没有影响的,基本按照score高低经行判断,总的来说加倍要赢是更接近于真实体验,不过挺多人乱加倍的,所以我暂时没作处理。

 

// ChuPai 出哪手牌的函数,如果返回[]int{}则一直等待,等系统自动出牌

// 出牌函数,我大了然后要出牌了,输入牌的slice、上家的手牌数量、下家的手牌数量、庄家的位置(我是0,下家是1,上家是2),输出要出的牌

func ChuPai(card []int32, theLastPlayerCardNum, theNextPlayerCardNum, diZhuSeat int32) []int32

重要函数之一,基本就是看自己的策略,和下家是不是盟友决定了。

唯一复杂的是,由于没有做score的动态调整,所以整个出牌逻辑压力就都集中在这里了。其他两名玩家的手牌数量不同,我们需要做出不同的局面判断。

大概就是拉一个表,由:

我的身份 下家身份  上家手牌数  下家手牌数  我的策略

0,1   0,1   1 ,2>2 1 ,2>2 1,2,3

进行排列组合,然后填写出牌逻辑了。

因为是自己出牌,所以选择余地较大,不会太破坏自己的最优牌型。

(注:具体组合过多,还是代码见吧,核心判断在于对手直接胜利的风险高低,则不一定按最高分值出牌。)

 

// GenPai 跟哪手牌的函数,如果返回[]int{}则表示PASS(所有之中最复杂的函数了)

// 跟牌函数,别人出牌我要出什么,输入牌的slice、上家的手牌数量、下家的手牌数量、庄家的位置(我是0,下家是1,上家是2),底牌的slice,底牌的位置(谁出的),输出要出的牌

func GenPai(card []int32, theLastPlayerCardNum, theNextPlayerCardNum, diZhuSeat int32, diPaiSeat int32, diPai []int32) []int32

这个其实和出牌考虑的事项是差不多那么多的,只是处理条件更为复杂了。

先需要一个

// 把玩家出的牌,找出其牌型

func paitype(p []int) string

找出底牌是什么类型。

 

// 找出手牌所有能凑出的单牌

func danpai(a [15]int) [][]int

 

然后找出所有能管上底牌的牌,全都要找。

然后一个一个试,基本来说,我是以-95-155为分界线,

全攻则不能亏95分,均衡则不能亏155分,全守就不用管了,烂命一条,破罐子破摔咯~

 

但事实上,根据对手直接胜利的风险大小,策略12的亏损额度可以一直往上涨,直到不惜代价管上为止。

(注:组合还是不少,依然代码见吧)

 

一些具体事项:

1.              由于2以上是正分,所以有时候为了获得更高的分,会强行拆小对子,而不出2。这时候就要让2贬值了,让对子不能拆,同样,小王大王也要相应的贬值,要做的即不比出2差,也不能把对子拆了。

2.              自己队友出大牌了,就要根据自己的牌的好坏决定要不要管,基本策略是牌越好管的越宽。

3.              策略的排列组合是永远在增加的过程,例如剩4张,剩5张也需要特殊处理的。只是情况并不多而已。

最好的方式当然是动态调整分数,让分数加上玩家的手牌数参数,但事实上功能是接近的,调整分数是能让策略更简洁漂亮,但是抽象的难度只会增加不会减少。

    4.    通用的函数是出牌(大中小)、牌(大中小)分别代表出牌和跟牌的策略123对应的判断逻辑。其中好牌和差牌(策略13)都是相对比较好判断的,不好不差的时候,才是最难判断的。现实中也一样,牌不好也不差的时候,最纠结的就是该让自己走牌轻松,让自己容易赢,还是破坏地主,让队友赢。因为队友的牌是未知的,万一选择错误,很有可能把本来可以赢的牌打输了。

5.         亏损的承受能力,比牌型的分数更加没有依据,明显和我的目的,把感觉提炼成数字违背了。但是牌型

分是通过前面好多场斗地主而提炼出来的,而亏损承受能力,要先有评分标准之后,才能观察出牌情况,才有亏损承受能力数据,简单来说,就是功夫不到位,产出自然不会凭空出现的。

   

完整斗地主AI代码链接https://pan.baidu.com/s/1c3yuCI

 

本篇相关:ddz1.go(主体文件)findAllCanGenPai.go  findCardType.go  ddz1_test.go(测试文件)

主要内容: 出牌、跟牌函数

判断牌型函数

找出所有能管上的牌型函数

Test函数(附洗牌发牌函数)

golang语言编写)

 

很可惜的是,在这个项目即将完成的时候,公司解散了,这一篇文章算是遗产吧。


 特别鸣谢heather女神的提点、支持及审核。

 如需进一步了解,可联系QQ714928385(统一)。

 

 



 

                                                                

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签: