以斗地主AI为例,探讨数值体系的设计和后期调整方案(一)
以我对数值策划的浅显体会,我认为数值策划主要做两件事:
一、 why
我为什么要做成这种体验
二、 how
我如何做出这种体验
很可惜,暂时大部分的why我都无法说出逻辑清晰的所以然,于是我只能在鬼蟹的“lol平衡性调整”中看的很爽,但无法结合实际的东西加以分析。
不过,以斗地主为例的话,就很容易说清楚这个why了,我要做的就是尽可能把AI做的像真人一样,让玩家觉得时刻都能找到“人”陪他玩。
于是,顺理成章可以进入我比较擅长的how的部分了。
首先,how对应的对象是机器人、是程序、是死物,对它们而言,是没有“感觉不对”、“感觉慢了”这类的说法,对他们而言只有条件判断if和数值(数字)。以斗地主为例,我们要把平常的出牌经验,全部抽象成为数字,例如我说我的牌好,要转化成分数高;出这手牌好,要转化成为,我(或者我的队友)的分数变高了,或者对手的分数降低了;这手牌能不能出,要转化为,出了之后收益多大,但让对手胜利的概率又有多大;等等。
这篇文章讲的就是
1. 如何把场上的条件转化为数字(数值体系的建立)
2. 遇到很难转化的情况如何额外处理(附加判定机制)
3. 前期(整个游戏不能跑起来之前)如何调整数值
4. 后期(可以亲身玩游戏之后)面对各种疑难杂症,又如何调整数值,甚至推翻原来的体系。
5. 附加内容:做出一个能跑起来的斗地主程序,有发牌、叫地主、出牌直到一局结束。
(注:由于目标是嵌套到已存在的斗地主程序里面,而程序员希望他要做的事情越少越好,所以这个斗地主是一个不完整版,它缺少记录出牌流程并加以分析的模块,只有对应当前的状态,选择合适的出牌策略的流程。不过测试结果已经比较接近真人的行为了,完整版将会在未来补全。并且牌的数值代号是以他的规则定义的,而不是最合适AI设计的定义,所以需要有一些转换的函数,只是看起来同一张牌有多个数值定义,但对实际功能是没有任何影响的)
——————————————正文分割线———————————————————
第一步:定义我们需要的基础信息(注:现在只是罗列,之后会逐行解析)
// Pai 手牌的信息
type Pai struct {
handcard []int //原始的牌101-1402
initstate InitState //初始状态
arrstate []FinalState //基本遍历所有情况
bestcardtype [][]int //最好的手牌分配
bestscore int //最好的手牌分配的分数
tactics int //确定出牌的策略,1:优势牌全攻,2:中势牌半攻半守,3:劣势牌,全防守
dizhuseat int //地主的位置(我的位置是0,下家的位置是1,上家的位置是2)
myseat string //我对应地主的位置
bigcardscore int //手上大牌的分数,用来确定要不要叫地主
}
原始的牌是101-104(四个A),1301-1304(四个K),1401小王,1402大王。
设计是很清晰明了的,但是我们AI是不看花色的,只分大小王,而且A是比K大的。
所以我们的排序将成为3-13,14=A,15=2,16=小王,17=大王
(注:即3,4,5,6,7,8,9,10,11,12,13,14,15,16,17)
当然这样的分配也有个问题,顺子是不能A2,2小王这样顺的,不过我们会在函数里面做判断。
同时,我们还要转化为一个数组,把牌放进入。
arrpai [15]int 我们一共15个大小的牌,找15个盒子把他们装起来,盒子最少装0个,最多装4个。
虽然这样我们看起来不够清晰,但是增删改查起来要更方便,这是我们最常操作的数据了。
第一步,我们要让AI学会“看牌”,就是把牌分好类让他记住。
// InitState 手牌的初始牌型分布
type InitState struct {
arrpai [15]int //把手牌变成数组,方便统计
shunzi5 []int //只记录最小的那张牌,例如[3,4,5,6,7]只记录3,变成[3],后续再append
shunzi6 []int
shunzi7 []int
shunzi8 []int
shunzi9 []int
shunzi10 []int
shunzi11 []int
shunzi12 []int
liandui3 []int
liandui4 []int
liandui5 []int
liandui6 []int
liandui7 []int
liandui8 []int
liandui9 []int
liandui10 []int
sanshun2 []int
sanshun3 []int
sanshun4 []int
sanshun5 []int
sanshun6 []int
zhadan []int
arr [22][]int //0-7存放顺子,8-15存放连对,16-20存放三顺(三顺一般都进入飞机组了),21放炸弹
state FinalState //把上面的,信息,归纳进入最终状态
}
第一步:我们要把原来的数组,转化为新的数组。
func changeToArr(pai []int) [15]int // 把后端发来的牌,转化成[15]int,方便处理
(注:所有函数代码都会在文章末段贴出,本文主将数值部分,所以代码部分可看可不看)
第二步:我们要找出所有长的特殊牌型,顺子、连对、三顺和炸弹,并把他们存放起来。
(注:由于牌型是有规律的,所以只需要记录第一张 牌型的分类,就可以知道他的完整信息了)
第三步:创建一个arr [22][]int 把以上的所有信息都存放起来,这一步我们就把准备工作都做好了,炒菜!
(注:剩下的牌型会在下一步提炼出来,并且本版本炸弹并不会转化为4带2。)
// FinalState 遍历所有牌型分类,得出这一种牌型的最终状态
type FinalState struct {
finalpai [15]int //把手牌记录下来(复制arrpai)
finalarr [25][]int //在上面22种牌型的基础上,再添加三张、对子、单张
plane [][]int //把飞机找都出来(注:这时候就不能用一个[]int存放了,因为是无规则的
three [][]int //把三带一、三带一对、三张(但没东西带的)都找出来,此时所有牌型都有了。
clear [][]int //遍历出不能再细分的牌型。
score int //找出分类好的牌型,找出对应的分数,为了最好的牌型准备
}
FinalState最终状态,也就是我们把牌分的足够细了,不能继续分牌的状态。
有多少种分牌方法,就有多少种最终状态,这个时候我们不会在分牌的时候就把AA拆成A A,也不会把AAAK 拆成AA A K。不过未来出牌的策略,依然可能会拆开来出。
例如:{101,201,202,203,301,302,303,401,601,602,701,702,1101,1102,1103,1301}
(注:一个A,三个2, 三个3, 一个4,一对6,一对7,一个J,一对K)
其中一个finalState就会拆成:3334,66,77,11&11,13,14,15,15,15
(注:因为这种牌型比较简单,没有顺子,如果顺子多的话将会拆出十几种分类,或更多)
一个三带一,三对,2个单牌,三个2是不搭配单牌的,基本都是用来灵活出来。
然后,其实至今为止,依然是在准备材料,我们真正的想要的,是score,就是找出如何分配最高分。
下一篇,将会介绍,如何定义一手牌的分值,和一些简单的数值优化。
完整斗地主AI代码链接https://pan.baidu.com/s/1c3yuCI
本篇相关:struct.go findbest.go
主要内容:结构体定义
找顺子各种牌型出来的函数
遍历牌型分类函数
计分函数
找出最高分的牌型的函数
(golang语言编写)