以斗地主AI为例,探讨数值体系的设计和后期调整方案(二)
上一篇,我们谈到计算如何当前牌型的计分的问题,有前面的铺垫,终于走到本主题的核心部分了。查看上篇:以斗地主AI为例,探讨数值体系的设计和后期调整方案(一)
说点题外话,之前我发现,市面上成功项目的系统公开、技术公开还有一些,不过相关的数值体系公开真的少之又少。当时很是气愤,找一些特别想知道的信息怎么就那么难呢?
然而,做完这个AI设计第一版,测试效果还不错的时候。我突然觉得,整个代码公开是没所谓的(当然,写的太丑,没什么技术成分是关键),但是那些数字,每一个都是我玩几千把斗地主的心血,每改一个数字,都又几十把过去了。不舍得把他们交给别人(是嫁女儿的感觉么?)
但是,后来想通了,以后会写出更多、更完善、更有价值的数值设计的,现在自己看来的宝石,再未来的自己看来也只是一个路边的石头罢了。抛弃过去,才能早点走向未来。
这时候就离不开代码的展示了,整个体系核心数值部分基本都在这一段代码里面。
// 注:这是第一版的数值效果。可以说是凭感觉随手写出来的,反正当时想着先出来,再慢慢改嘛,不过实际效果,这个体系虽然不够完整,但足以作为一个判断依据了,代码如下。
// 把牌型算分,配合上面的找出牌型
func paixingscore(i []int) int {
var score int
if len(i) == 1 { //单牌的算分
switch i[0] {
case 3, 4, 5, 6:
score = -100
case 7, 8, 9, 10:
score = -80
case 11, 12:
score = -55
case 13:
score = -35
case 14:
score = -15
case 15:
score = 30
case 16:
score = 60
case 17:
score = 100
default:
}
}
if len(i) == 2 { //对子的算分
switch i[0] {
case 3, 4, 5, 6:
score = -95
case 7, 8, 9, 10:
score = -75
case 11, 12:
score = -55
case 13:
score = -25
case 14:
score = -10
case 15:
score = 60
default:
}
}
if len(i) == 3 && i[0] == 15 { //三张2的算分
score = 90
}
if len(i) == 4 && i[0] == i[3] { //炸弹的算分
score = 120
}
if (len(i) == 4 && i[0] != i[3]) || (len(i) == 5 && i[0] == i[1]) { //三带2或者三带1算分
switch i[0] {
case 3, 4, 5, 6:
score = -60
case 7, 8, 9, 10:
score = -35
case 11, 12, 13:
score = -10
case 14:
score = 25
default:
}
}
if len(i) == 5 && i[0] != i[1] { //顺子5个的算分
switch i[0] {
case 3, 4, 5, 6:
score = -40
case 7, 8:
score = -20
case 9:
score = -05
case 10:
score = 10
default:
}
}
return score
}
1. 这个的思路核心就是一张3(小牌)和一张大王(绝对大牌)是等值的。
(注:后来有改动)
并且以手上的牌能否获得收益为价值依据,例如一张A虽然很大可能能打出去,但是不太可能获得上手机会,所以价值是负的,我是大部分时候宁愿少这张牌的。
而三个A带一,则被打的几率很小,上手的几率很大,我可以接受手上多这张牌的,所以判断为正分。
2. 只简要的计算区段,不作明显的细分
(注:这样建模方便,但是最终肯定要没一张牌细分的,从实际效果看来,不细分还凑合)
3. 对子要比单牌稍强,实际战斗中,管上单牌的概率比管上对子的要高,但主要还是看大小。
4. 6张以上的牌型,基本认定为没机会管上,也不会被管的牌型,所以他们是0分,
(注:实际不仅仅能用0分来概括的,但大部分实战确实能简化)
5. 三带1(2),分值比单牌对子远远提升,而且涨幅很高,例如三个A已经从负分转为正分了。
6. 炸弹的分大约是大王的120%,不分炸弹大小。
(注:这个设置确实差不多了,炸弹吃大王确实是一个数值陷阱,让人觉得炸弹好厉害,但实际效果并不是那么强,高估炸弹很容易有不合理的分数判断)
// 这个是测试后,通过反馈的问题优化的函数
// 把牌型算分
func paixingscore(i []int) int {
var score int
if len(i) == 1 { //单牌的算分
switch i[0] {
case 3, 4, 5, 6:
score = -100
case 7, 8, 9, 10:
score = -80
case 11, 12:
score = -55
case 13:
score = -35
case 14:
score = -15
case 15:
score = 35
case 16:
score = 70
case 17:
score = 110
default:
}
}
if len(i) == 2 { //对子的算分
switch i[0] {
case 3, 4, 5, 6:
score = -95
case 7, 8, 9, 10:
score = -75
case 11, 12:
score = -55
case 13:
score = -25
case 14:
score = -5
case 15:
score = 70
default:
}
}
if len(i) == 3 && i[0] == 15 { //三张2的算分
score = 105
}
if len(i) == 4 && i[0] == i[3] { //炸弹的算分
score = 150
}
if (len(i) == 4 && i[0] != i[3]) || (len(i) == 5 && i[0] == i[1]) { //三带2或者三带1算分
switch i[0] {
case 3, 4, 5, 6:
score = -59
case 7, 8, 9, 10:
score = -29
case 11, 12, 13:
score = -9
case 14:
score = 25
default:
}
}
if len(i) == 5 && i[0] != i[1] { //顺子5个的算分
switch i[0] {
case 3, 4, 5, 6:
score = -40
case 7, 8:
score = -20
case 9:
score = -5
case 10:
score = 10
default:
}
}
return score
}
我们对比一下改变的数值。
1. 2、小王、大王的分值提高了。
原因:看起来都是一手牌换一手牌,打一个3,回一个王,又回到起点了。
但其实,大牌上手并不一定要出小牌,可能会出一些长牌,留小牌最后出。这样小牌的价值就可以忽略了。
也就是总体而言,能上手的牌的价值有额外加成。
同样的,2 小王 <大王,虽然很多时候2 小王也可以上手一次,但是没有大王那样一锤定音。
2. 对2的价值因为单牌提高而提高了,因为对2拆开来也是很强大的。
但是对A的价值却逆增长,因为对A能称为大牌的时候,远比单张A多很多。而且拆对2很正常,拆对A是不够合理的。未来出牌的时候会有拆牌的需求出现,这时候就要在定分的时候调整了。
3. 三带1(2)的算分出现不整齐的数字(9),由于是第一版,所以数字都按爱好用5,10表示。
而三带1为什么要特殊出现9呢?
因为三带1在前期是小牌,被管上的可能性很大。
但是在后期,三带1就称为长牌了,很有可能称为值为0的牌。所以要做一个标记,让后期出牌能轻易的找出三带1作为清手牌的时机打出。
思考:这个策略最大的问题是什么?
缺陷1:没有动态调整,每个牌的价值,都会因为时间的推移变得不一样,例如大小王出完了,2的价值就提升了。应该要设计一个函数,通过计算各玩家出牌流程,然后调整手牌的价值。不过由于现在这个版本是没有记录功能的,所以动态调整将会在出牌(跟牌)的函数建立额外的判断条件。
缺陷2:手牌的价值,除了大小以外,还有牌型数的概念,即最少能出几次牌才能打完。例如到你出牌了,你宁愿剩下1张3,而不是一张A加一张2。而我把他们合在一起统计价值了。同上,如果有记录的话,把价值拆分,反而更好的调整两者的价值变化(注:大小价值变化慢,牌型数价值提升快)
结尾语,这一篇是大体的介绍了整体的策略变动和缺陷,下一篇将会讲解:
有分数了,但怎么利用,还需要添加哪些特殊变化的处理(策略)。
按这个策略实际打牌时会出现哪些问题(特殊情况)。
这些情况如何分析,如何处理。
详见:以斗地主AI为例,探讨数值体系的设计和后期调整方案(三)
完整斗地主AI代码链接https://pan.baidu.com/s/1c3yuCI
本篇相关:findbest.go API.go fuzhufunc.go
主要内容:判断最优牌型函数
后端接口的API函数
辅助函数(例如数组转换的函数)