新版太阳神三国杀武将技能AI设计概要
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
新版太阳神三国杀武将技能AI设计概要
目录
第一章:AI概述
第二章:触发技与响应询问
一、响应技能发动询问
二、响应卡牌使用询问
三、响应卡牌打出询问
四、响应选择询问
五、响应角色选择询问
六、响应卡牌选择询问
七、响应五谷丰登选牌询问
八、响应卡牌展示询问
九、响应花色询问
十、响应遗计询问
十一、响应拼点询问
十二、响应弃牌询问
第三章:视为技与技能发动
一、一般视为技与卡牌使用
二、一般视为技与卡牌响应
三、判定锁定视为技
四、使用技能卡
第四章:AI特征信息
一、使用价值与使用优先级
二、相合花色与相合卡牌
三、武将嘲讽值
四、仇恨值与角色关系
第五节:常用AI函数
第一章AI概述
正如《新版太阳神三国杀武将扩展学习手册》所描述的那样,太阳神三国杀的AI系统还是有一定章法可循的。
武将技能AI作为AI系统的重要组成部分,自然也不例外。
手册第八章里简单介绍了AI的概念和作用,给我们留下了一个初步的印象;这里将更进一步,感受一下更具体的武将技能AI设计工作。
说起来,武将技能AI还是与具体的武将技能有着密不可分的联系。
技能的种类、传递数据的方式,许多因素都影响着武将技能AI的设计。
触发技有触发技的AI策略,视为技也有自己的一套AI实现方法。
具体到技能上,更是花样百出,各有不同。
不过,掌握了武将技能AI的工作方法,再进行具体的设计,就容易很多了。
一般而言,距离修改技、手牌上限技之类的技能,在游戏里以锁定技的样貌出现,不需要玩家干预。
不论操作者是人类玩家还是电脑玩家,都是同样的效果,所以这些技能理论上是不需要AI支持的。
对于触发技来说,电脑玩家只需要知道在需要作出选择的时候,如何找出正确的答案即可。
为此,AI系统提供了一系列的决策表和决策函数。
每当遇到触发技询问时,首先查阅决策表中是否有对应的决策函数。
若找到这样的函数,就根据函数的结果作出选择;若没有找到,就交给AI系统统一处理,得出一个一般情形下的选择结果。
所以,触发技AI的设计,其实就是针对技能中出现的各个询问场景,编写对应决策函数的过程。
每个询问场景都会有自己特定的决策函数格式,按照这些格式写决策函数,就会很容易设计出触发技的AI了。
而对于视为技来说,情况又有所不同。
视为技没有特定的触发时机,需要玩家主动选择发动。
这需要考虑两个方面:什么时候发动,以及要以怎样的方式发动。
在这个过程中,AI系统会依次检查玩家拥有的卡牌,判断哪些卡牌可用,并参照可用卡牌的优先级、使用价值等特征信息,选出当前最应使用的卡牌(包括技能卡和实际的卡牌),按照对应的卡牌使用方式使用它。
每当使用完一张卡牌,再重复这个过程,直到不再有卡牌可用。
这样,视为技的AI设计,一方面要提供判断卡牌是否可用的检测函数,一方面要提供包含了卡牌使用方式的执行函数。
此外,还要提供一些相关的特征信息,以决定在所有卡牌使用过程中的出场顺序。
除了武将技能AI以外,AI系统还包括了身份识别等更为丰富的内容,这里就不再关注了。
关于AI系统的更为详尽的介绍,可以参考太阳神三国杀extension-doc文件夹中的11-Fundamentals.lua~18-Beta.lua等文档。
第二章触发技与响应询问
一、响应技能发动询问
技能代码中出现askForSkillInvoke()函数的地方就是询问是否发动技能的场景了。
它的函数原型是:
Room:askForSkillInvoke(player, skill_name, data)
其中:
player:ServerPlayer类型,表示被询问发动技能的当前角色。
skill_name:string类型,表示技能的名字。
data:QVariant类型,表示传递给AI系统的参考数据。
另一个函数原型是:
ServerPlayer:askForSkillInvoke(skill_name, data)
参数skill_name和data的涵义与第一个原型中的同名参数是相同的。
对于这个场景,AI系统的处理函数是:
SmartAI:askForSkillInvoke(skill_name, data)
这个函数首先将通过表sgs.ai_skill_invoke查找对应的决策函数,而这个决策函数正是我们要具体设计的内容。
响应技能发动询问的决策函数原型是:
function(self, data)
其中:
self:表示表SmartAI,由AI系统提供。
data:表示参考数据,其实就是技能代码中传递过来的data参数了。
它的执行结果是bool类型的。
如果为true,表示发动技能;为false的话,则不发动这个技能。
这个表self(或者说表SmartAI)提供了许多有用的信息,比如:
self.player就是Room:askForSkillInvoke(player, skill_name, data)中的第一个参数player,ServerPlayer类型,表示当前响应询问的角色。
self.room表示当前所在的游戏房间,Room类型。
self.friends表示当前角色的所有友方角色,也包括自己在内,table类型。
这里的友方不一定意味着身份的相同,主公与忠臣、忠臣与内奸、主公与内奸、内奸与反贼之间在一定时段內都可能会是友方关系。
self.friends_noself表示当前角色的除自身以外的所有友方角色,table类型。
self.enemies表示当前角色的所有敌方角色,table类型。
同self.friends的情形一样,身份的差异也不一定意味着阵营的对立。
将我们的决策函数写入表sgs.ai_skill_invoke的方法是:
sgs.ai_skill_invoke[skill_name] = function(self, data)
这就将技能名skill_name与特定的决策函数对应起来了。
如果技能是否发动与决策无关,那么决策函数可以简化成一个对应的bool 值,比如:
sgs.ai_skill_invoke[skill_name] = true
就表示技能skill_name始终发动。
如果SmartAI:askForSkillInvoke(skill_name, data)函数没有找到所需要的决策函数,那么它将按照默认的方式处理这个场景。
即,如果技能skill_name的触发频率是sgs.Skill_NotFrequent,那么产生一个false结果,不发动这个技能;如果触发频率是sgs.Skill_Frequent,那么产生一个true结果,发动技能。
对于其它场景,AI系统也是大体依照这样的流程进行处理的,后文不再赘述。
二、响应卡牌使用询问
技能代码中出现askForUseCard()函数的地方就是询问使用卡牌的场景了。
函数原型是:
Room:askForUseCard(
player, pattern, prompt, notice_index=-1, method = sgs.Card_MethodUse )
其中:
player:ServerPlayer类型,表示被询问使用卡牌的当前角色。
pattern:string类型,表示卡牌应符合的条件。
prompt:string类型,表示询问时出现的提示信息。
notice_index:number类型,表示提示信息的编号,默认为-1。
method:sgs.Card_HandlingMethod类型,表示卡牌的处理方式,默认是sgs.Card_MethodUse。
对于这个场景,AI的处理函数是:
SmartAI:askForUseCard(pattern, prompt, method)
三个参数pattern、prompt、method和技能代码中的同名参数是相同的。
这个函数通过表sgs.ai_skill_use来查找决策函数。
响应卡牌使用询问的决策函数原型是:
function(self, prompt, method)
它的执行结果是一个string,表示卡牌的使用方式。
具体的格式是:
#N:C:->U
#N[S:P]:C:->U
@N=C->U
@N[S:P]=C->U
其中:
N表示卡牌的对象名(objectName())。
S表示卡牌的花色字符串(getSuitString())。
P表示卡牌的点数(getNumber())。
C表示卡牌的子卡构成,是由一系列通过“+”连接的卡牌编号(getId())组成的字符串。
”.”表示没有子卡。
U表示卡牌的使用对象构成,是由一系列通过“+”连接的角色的对象名(objectName())组成的字符串。
”.”表示不指定使用对象。
以“#”开始的string结果表示Lua卡牌的使用方式,而以“@”开始的string 结果表示C++卡牌的使用方式。
因此我们在设计武将技能AI时,写出的卡牌的使用方式,都是以“#”开始的。
将决策函数写入表sgs.ai_skill_use的方法是:
sgs.ai_skill_use[pattern] = function(self, prompt, method)
如果SmartAI:askForUseCard(pattern, prompt, method)函数没用找到这个决策函数的话,默认产生的结果是”.”,表示不使用任何卡牌。
三、响应卡牌打出询问
技能代码中出现askForCard()函数的地方就是询问打出卡牌的场景了。
这个函数的原型是:
Room:askForCard(player, pattern, prompt, data, skill_name)
以及
Room:askForCard(
player, pattern, prompt, data, method = sgs.Card_MethodDiscard,
to=NULL, isRetrial = false, skill_name
)
其中:
player:ServerPlayer类型,表示被询问打出卡牌的当前角色。
pattern:string类型,表示卡牌应符合的条件。
prompt:string类型,表示询问时出现的提示信息。
data:QVariant类型,表示参考数据,还是从技能代码里传递过来的。
skill_name:string类型,表示技能的名字。
method:sgs.Card_andlingMethod类型,表示对卡牌的处理方式,默认是sgs.Card_MethodDiscard。
to:ServerPlayer类型,默认为nil。
isRetrial:bool类型,表示是否用于改判,默认为false。
对于这个场景,AI的处理函数是:
SmartAI:askForCard(pattern, prompt, data)
这个函数通过表sgs.ai_skill_cardask来查找决策函数。
响应卡牌打出询问的决策函数原型是:
function(self, data, pattern, target, target2)
和响应卡牌使用询问的决策函数一样,它的结果也是string类型的,表示卡牌的使用方式。
将决策函数写入表sgs.ai_skill_cardask的方法是:
sgs.ai_skill_cardask[prompt] = function(self, data, pattern, target, target2)
如果SmartAI:askForCard(pattern, prompt, data)函数没有找到这个决策函数,AI系统会尝试着按照pattern和prompt决定打出哪些卡牌。
如果仍然找不到符合条件的卡牌,那么将产生”.”的结果,表示不打出任何卡牌。
四、响应选择询问
技能代码中出现askForChoice()函数的地方就是询问选择的场景了。
这个函数的原型是:
Room:askForChoice(player, skill_name, choices, data)
其中:
player:ServerPlayer类型,表示被询问选择的当前角色。
skill_name:string类型,表示技能的名字。
choices:string类型,表示各个选项,是由一系列通过”+”连接的选项字符串组成的。
data:QVariant类型,表示要向AI传递的参考数据。
对于这个场景,AI系统的处理函数是:
SmartAI:askForChoice(skill_name, choices, data)
这个函数通过表sgs.ai_skill_choice来查找决策函数。
响应选择询问的决策函数原型是:
function(self, choices, data)
它将产生一个string类型的结果,表示选出的那个选项。
将决策函数写入表sgs.ai_skill_choice的方法是:
sgs.ai_skill_choice[skill_name] = function(self, choices, data)
如果选择的结果与决策无关,那么决策函数可以直接简化成这个结果,比如:sgs.ai_skill_choice[skill_name] = “choiceA”
就表示技能skill_name询问选择时,始终选择choiceA。
如果SmartAI:askForChoice(skill_name, choices, data)函数仍然没有找到这个决策函数,那么AI系统首先将检查这个技能是否有默认的选择,如果有,就直接用默认的选择作为结果。
否则,AI系统将从choices里随机选出一个选项,作为最终的结果。
五、响应角色选择询问
技能代码中出现askForPlayerChosen()函数的地方就是询问选择一名角色的场景了。
这个函数的原型是:
Room:askForPlayerChosen(player, targets, reason)
其中:
player:ServerPlayer类型,表示被询问选择角色的当前角色。
targets:QList<ServerPlayer *>类型,是一个列表,表示各个备选的角色。
reason:string类型,表示询问角色选择的原因。
对于这个场景,AI系统的处理函数是:
SmartAI:askForPlayerChosen(targets, reason)
这个函数通过表sgs.ai_skill_playerchosen来查找决策函数。
响应角色选择询问的决策函数原型是:
function(self, targets)
它将产生一个ServerPlayer类型的结果,表示被选出的角色。
将决策函数写入表sgs.ai_skill_playerchosen的方法是:
sgs.ai_skill_playerchosen[reason] = function(self, targets)
如果SmartAI:askForPlayerChosen(targets, reason)找不到这样的决策函数,那么AI系统将从这些备选角色中随机选择一名,作为最后的结果。
六、响应卡牌选择询问
技能代码中出现askForCardChosen()函数的地方就是询问选择一张卡牌的场景了。
这个函数的原型是:
Room:askForCardChosen(player, who, flags, reason)
其中:
player:ServerPlayer类型,表示被询问选择卡牌的当前角色。
who:ServerPlayer类型,表示待选卡牌所属的目标角色。
flags:string类型,表示待选卡牌的位置标志。
由”h”(表示手牌区)、”e”(表示装备区)、”j”(表示判定区)组合而成。
reason:string类型,表示询问卡牌选择的原因。
对于这个场景,AI系统的处理函数是:
SmartAI:askForCardChosen(who, flags, reason)
这个函数通过表sgs.ai_skill_cardchosen来查找决策函数。
响应卡牌选择询问的决策函数原型是:
function(self, who, flags)
它将产生一个number类型的结果,表示选出的卡牌的编号。
将决策函数写入表sgs.ai_skill_cardchosen的方法是:
sgs.ai_skill_cardchosen[reason] = function(self, who, flags)
如果选择的结果与决策过程无关,那么可以决策函数可以简化为具体的卡牌编号。
比如:
sgs.ai_skill_cardchosen[reason] = 23
就表示在响应原因为reason的卡牌选择询问时,始终选择编号为23的卡牌(杀[♥10])。
如果SmartAI:askForCardChosen(who, flags, reason)找不到这个决策函数,那么AI系统将根据who是否为友方、flags包含的区域以及具体的reason作出一般情形下的选择。
这个选择的过程较为复杂,具体可参考smart-ai.lua中该函数的具体代码。
七、响应五谷丰登选牌询问
技能代码中出现askForAG()函数的地方就是询问从五谷丰登界面选择一张卡牌的场景了。
这个函数的原型是:
Room:askForAG(player, card_ids, refusable, reason)
其中:
player:ServerPlayer类型,表示被询问五谷丰登选牌的当前角色。
card_ids:QList<int>类型,表示五谷丰登界面中所有备选卡牌的编号列表。
refusable:bool类型,表示是否可以拒绝响应此询问。
reason:string类型,表示询问选择的原因。
对于这个场景,AI系统的处理函数是:
SmartAI:askForAG(card_ids, refusable, reason)
这个函数通过表sgs.ai_skill_askforag来查找决策函数。
响应五谷丰登选牌询问的决策函数原型是:
function(self, card_ids)
它将产生一个number类型的结果,表示选出的卡牌的编号。
将决策函数写入表sgs.ai_skill_askforag的方法是:
sgs.ai_skill_askforag[reason] = function(self, card_ids)
如果SmartAI:askForAG(card_ids, refusable, reason)找不到这样的决策函数,那么AI系统将根据refusable、reason以及self.player自身的技能和对卡牌的需求程度作出一般情形下的选择。
这个选择的过程同样较为复杂,具体可以参考smart-ai.lua中该函数的具体代码。
八、响应卡牌展示询问
技能代码中出现askForCardShow()函数的地方就是询问展示一张卡牌的场景了。
这个函数的原型是:
Room:askForCardShow(player, requestor, reason)
其中:
player:ServerPlayer类型,表示被询问展示卡牌的当前角色。
requestor:ServerPlayer类型,表示发起询问的源角色。
reason:string类型,表示询问卡牌展示的原因。
对于这个场景,AI系统的处理函数是:
SmartAI:askForCardShow(requestor, reason)
这个函数通过表sgs.ai_cardshow来查找决策函数。
响应卡牌展示询问的决策函数原型是:
function(self, requestor)
这个函数将产生一个Card类型的结果,表示将展示的卡牌。
将决策函数写入表sgs.ai_cardshow的方法是:
sgs.ai_cardshow[reason] = function(self, requestor)
如果SmartAI:askForCardShow(requestor, reason)没用找到这样的决策函数,那么AI系统将从当前角色的手牌中随机选择一张作为结果以进行展示。
九、响应花色询问
技能代码中出现askForSuit()函数的地方就是询问选择一种花色的场景了。
这个函数的原型是:
Room:askForSuit(player, reason)
其中:
player:ServerPlayer类型,表示被询问选择花色的当前角色。
reason:string类型,表示询问花色的原因。
如果reason缺失的话,AI系统会将其补全为”fanjian”,并依照反间选择花色的情形处理。
对于这个场景,AI系统的处理函数是:
SmartAI:askForSuit(reason)
这个函数通过表sgs.ai_skill_suit来查找决策函数。
响应卡牌展示询问的决策函数原型是:
function(self)
这个函数将产生一个number类型的结果,表示选出的花色编号。
其中,0、1、2、3分别表示黑桃、红心、草花、方块花色。
将决策函数写入表sgs.ai_skill_suit的方法是:
sgs.ai_skill_suit[reason] = function(self)
如果SmartAI:askForSuit(reason)没用找到这样的决策函数,那么AI系统将从0~3中随机选出一个数字作为最后选择的结果。
十、响应遗计询问
技能代码中出现askForYiji()函数的地方就是询问遗计分牌的场景了。
这个函数的原型是:
Room:askForYiji(guojia, cards, is_preview=true, visible=false)
其中:
guojia:ServerPlayer类型,表示被询问遗计分牌的当前角色。
cards:QList<int>类型,表示待分配的卡牌的编号列表。
is_preview:bool类型,默认为true。
visible:bool类型,表示卡牌在分配过程中是否对所有角色可见,默认为false。
原本这个函数只是技能“遗计”专用的,后来随着“礼让”等技能的出现,才开放出来,有了进行额外的AI设计的可能。
对于这个场景,AI系统的处理函数是:
SmartAI:askForYiji(card_ids, reason)
这个函数是通过表sgs.ai_skill_askforyiji来查找决策函数的。
响应遗计询问的决策函数原型是:
function(self, card_ids)
这个函数将产生两个结果,依次是ServerPlayer类型(表示分牌的目标角色)和number类型(表示分给目标角色的卡牌的编号)的。
将决策函数写入表sgs.ai_skill_askforyiji的方法是:
sgs.ai_skill_askforyiji[reason] = function(self, card_ids)
如果SmartAI:askForYiji(card_ids, reason)没有找到这个决策函数,那么AI系统将根据self.player自身存在的标志,采用“礼让”或“遗计”的策略产生分牌的结果。
这部分内容较为复杂,具体可以参考smart-ai.lua中此函数的相关代码。
十一、响应拼点询问
技能代码中出现askForPindian()函数的地方就是询问打出一张卡牌拼点的场景了。
这个函数的原型是:
Room:askForPindian(player, from, to, reason)
其中:
player:ServerPlayer类型,表示被询问打出卡牌拼点的当前角色。
from:ServerPlayer类型,表示发起拼点的角色。
to:ServerPlayer类型,表示被拼点的角色。
reason:string类型,表示拼点的原因。
对于这个场景,AI系统的处理函数是:
SmartAI:askForPindian(requestor, reason)
这个函数通过表sgs.ai_skill_pindian查找决策函数。
响应拼点询问的决策函数原型是:
function(minusecard, self, requestor, maxcard, mincard)
其中:
minusecard:Card类型,由AI系统产生的当前角色手牌中点数最小的卡牌。
self:表示由AI系统提供的表SmartAI。
requestor:ServerPlayer类型,表示发起拼点的角色。
maxcard:Card类型,由AI系统产生的当前角色手牌中使用价值不高的卡牌中点数最大的卡牌;或者就是minusecard。
mincard:Card类型,由AI系统产生的当前角色手牌中使用价值不高的卡牌中点数最小的卡牌;或者就是minusecard。
这个函数将产生一个Card类型的结果,表示将打出的用于拼点的卡牌。
将决策函数写入表sgs.ai_skill_pindian的方法是:
sgs.ai_skill_pindian[reason] = function(minusecard, self, requestor, maxcard, mincard)
如果SmartAI:askForPindian(requestor, reason)没有找到这样的决策函数,那么AI系统将根据requestor是否为友方角色,来决定使用mincard或maxcard作为产生的结果。
十二、响应弃牌询问
技能代码中出现askForDiscard()的地方就是询问弃置卡牌的场景了。
这个函数的原型是:
Room:askForDiscard(
target, reason, discard_num, min_num,
optional=false, include_equip=false, prompt
)
其中:
target:ServerPlayer类型,表示被询问弃牌的当前角色。
reason:string类型,表示弃牌的原因。
discard_num:int类型,表示应当弃牌的数目。
min_num:int类型,表示最少应弃牌的数目。
optional:bool类型,表示是否可以选择不弃牌,默认为false。
include_equip:bool类型,表示弃牌范围是否包括装备区,默认为false。
prompt:string类型,表示询问时出现的提示信息。
对于这个场景,AI系统的处理函数是:
SmartAI:askForDiscard(reason, discard_num, min_num, optional, include_equip) 它是通过表sgs.ai_skill_discard来查找决策函数的。
响应弃牌询问的决策函数原型是:
function(self, discard_num, min_num, optional, include_equip)
这个函数将产生一个table类型的结果,表示将弃掉的卡牌的编号名单。
将决策函数写入表sgs.ai_skill_discard的方法是:
sgs.ai_skill_discard[reason] = function(self, discard_num, min_num, optional, include_equip)
如果AI系统的处理函数没有找到这样的决策函数,那么将按照一般情形的弃牌策略产生结果。
这个决策的过程较为复杂,具体可参考smart-ai.lua中此函数的相关代码。
第三章视为技与技能发动
一、一般视为技与卡牌使用
一般视为技,也就是将一些卡牌视为一张已存在的卡牌的技能,大多是没用特定的触发时机的。
从第一章我们知道,AI系统通过不断检查卡牌是否可用来决定具体的使用方式;而在这个过程中,除了需要检查各个真实的卡牌,还需要检查那些可能通过视为技视为的虚拟卡牌。
这首先要求AI系统了解当前角色拥有哪些视为技,或者说,我们需要先将我们的一般视为技信息加入AI系统。
AI系统通过表sgs.ai_skills记录已经存在的视为技,而视为技的各种信息也是通过一个表来体现的。
所以最初的代码应该是:
local 视为技信息表对象= {}
视为技信息表对象[“name”] = “视为技名字”
table.insert(sgs.ai_skills, 视为技信息表对象)
第一句话创建了一个视为技信息表,用于记录一个视为技AI的各类信息。
第二句话指明了这个信息表的名字,表明其对应的视为技。
从此这个信息表就可以用来表示当前的这个视为技了。
第三句话是将信息表插入到表sgs.ai_skills,使得AI系统可以意识到这个视为技的存在,进而也有了发动它的可能。
在AI系统通过这三句话得知了视为技的存在后,接下来就是要检查包括视为的虚拟卡牌在内的各个卡牌是否可用了。
对于视为技AI来说,这一步是通过getTurnUseCard函数具体实现的。
这个函数的原型是:
function(self, inclusive)
其中:
self:其实就是表SmartAI自身。
inclusive:bool类型,仅在当前角色没有手牌时其值为true,否则为nil。
它将产生一个Card类型的结果,表示通过这个视为技视为的虚拟卡牌。
如果产生的结果为nil,那么AI系统将不会在出牌时考虑发动这个视为技。
经常我们会用sgs.Card_Parse()函数产生这个虚拟卡牌,函数的原型是:
Parse(str)
这里,唯一的参数str表示卡牌的构成方式,string类型。
对于一般视为技来说,它的具体格式是:
“N:K[S:P]=C”
其中:
N表示视为卡牌的对象名(objectName())
K表示视为技的名字
S表示视作卡牌的花色字符串(getSuitString())
P表示视作卡牌的点数字符串(getNumberString())
C表示卡牌的具体构成,是由一系列通过“+”连接的所用卡牌的编号组成的字符串。
因此,检查视为的卡牌是否可用,让AI系统考虑发动视为技的方法就是:视为技信息表对象.getTurnUseCard = function(self, inclusive)
由于一般视为技视作的是一种已经存在的卡牌,所以具体的使用方式,我们就不用额外给出了。
二、一般视为技与卡牌响应
有很多一般视为技都可以用于卡牌的响应,而不仅仅是在出牌阶段主动发动。
在响应卡牌需求时,AI系统要做的事情,就是判断一下哪些卡牌可以用于发动这个视为技,并且给出视作卡牌的具体构成方式。
当然,这个具体的判断过程,实际上也是一个函数,它的原型是:
function(card, player, card_place)
其中:
card:Card类型,表示当前判断的一张实际卡牌。
player:ServerPlayer类型,表示需要作出卡牌响应的当前角色。
card_place:sgs.Player_Place类型,表示当前这张实际卡牌所在的位置。
class_name:string类型。
如果处于card_place位置的卡牌card符合此视为技的要求,那么它将产生一个string类型的结果,表示所视作的卡牌的构成方式。
AI系统通过表sgs.ai_view_as来查找这个函数,方法是:
sgs.ai_view_as[skill_name] = function(card, player, card_place, class_name)
这里,用于指明此函数的skill_name,string类型,表示这个一般视为技的名字。
三、判定锁定视为技
锁定视为技作为视为技的一种特殊情形,在AI设计时,除了要像主动发动一般视为技那样,创建自己的信息表并记录到表sgs.ai_skills中之外,还要给出一个判定卡牌是否受到影响而改变的处理函数。
每次AI系统在检查所拥有的卡牌之前,都会先通过这个处理函数,将受影响的卡牌变更为其锁定视作的卡牌,再继续后续的工作。
这个处理函数的原型是:
function(card, card_place, player)
其中:
card:Card类型,表示当前判定的一张实际卡牌。
card_place:sgs.Player_Place类型,表示此判定的卡牌当前所在的位置。
player:Player类型,表示拥有此锁定视为技的角色,同时也是卡牌持有者。
如果卡牌的确受到此锁定视为技的影响,那么这个函数将产生一个string类型的结果,表示具体的影响方式。
和卡牌构成方式一样,格式是“N:K[S:P]=C”,只是这里的C必然只由一个卡牌编号组成而已。
四、使用技能卡
在武将扩展行为中,比起一般视为技,在视为技中使用技能卡的情形更为普遍。
不过同样地,这也需要先将相应的视为技加入AI系统,方法和一般视为技是一样的,通过sgs.ai_skills实现。
接下来也是通过getTurnUseCard函数让AI系统认识到这张技能卡的可用性。
而且我们通常也是用sgs.Card_Parse()函数产生一个虚拟的技能卡的。
不同的是,虽然这个Parse函数的原型没变,还是Parse(str),不过参数str的格式不再是一般视为技中的“N:K[S:P]=C”了,而是在“响应卡牌使用询问”时介绍的,技能卡自己的构成方式”#N:C:->U”(Lua技能卡)或者”@N=C->U”(C++技能卡)。
由于getTurnUseCard函数只是用来告诉AI系统考虑使用这张技能卡的,具体的使用方式,包括具体要用什么牌、对谁使用等等,通常不需要在这里写好,所以很多时候,参数str的内容只是被刻意简单地写成了:
“#N:.:”(对于Lua技能卡)
或者是
"@N=.->."(对于C++技能卡)
仅仅给出了技能卡的名字N,而子卡构成C和使用对象U都被忽略了。
在AI系统决定使用这张技能卡后,就要考虑具体的使用方法了。
显然这是需要我们提供的,而AI系统则提供了一个表sgs.ai_skill_use_func来记录这些具体的使用方法。
届时,AI系统通过查这个表,找到我们提供的使用方法,就可以真正发动技能使用技能卡了。
说到这个具体的使用方法,其实也表现为一个函数,这个函数的原型是:function(card, use, self)
其中:
card:Card类型,表示一张虚拟的技能卡,其实就是刚才那个getTurnUseCard 函数产生的结果。
比如sgs.Card_Parse(“#N:.:”),如果之前str参数被简单地写成”#N:.:”的话。
use:sgs.CardUseStruct类型,是一个卡牌使用结构体,表示技能卡的使用方法。
我们就是通过填充这个结构体来告诉AI系统如果按照我们的意愿正确使用技能卡的。
self:依然是SmartAI的代名词,不解释了。
这个函数并不显式地产生任何结果,因为结果已经通过填充参数use传达出去了。
比如,use.card表示要使用的技能卡,而use.to表示技能卡的使用目标。
在这里,只有当use.card被填充(不为nil)时,AI系统才会去真正使用这张技能卡。
将这个函数写入表sgs.ai_skill_use_func的方法是:
sgs.ai_skill_use_func[name] = function(card, use, self)
里面的那个name表示技能卡的名字,用于在表中区分各个使用方法函数。
关于这些触发技与视为技的AI设计,大体上就分为这些类型了,具体实例可参阅与本文拟配套的《新版太阳神三国杀武将技能AI实例分析》。
第四章AI特征信息
一、使用价值与使用优先级
对于使用技能卡的视为技来说,提供具体的使用方法只能令AI系统在游戏时按要求使用技能卡,但是很多时候我们同样关心包括技能卡在内的各类卡牌的使用次序。
比如,孙权先使用一张【无中生有】再发动制衡(使用制衡技能卡)可能就会比直接发动制衡(使用制衡技能卡)的效果更好。
这其实就涉及了卡牌的两个设计要素:使用价值和使用优先级。
卡牌是有自己的使用价值的,而AI系统通常倾向于先使用价值更高的卡牌。
在上面的例子中,无中生有的使用价值是10,而制衡卡的使用价值是9,所以一般情况下电脑玩家会先使用无中生有而不是直接制衡掉它。
这样一个容易想到的结果就是,如果不向AI系统给出我们的技能卡的使用价值,那么这些技能卡应该总是被最后使用的。
表sgs.ai_use_value被用来记录各个卡牌的使用价值,比如我们可以用形如。