2024年8月12日发(作者:)
在cpp区发了那篇DIY教程贴之后,我转战到了lua区,发现大家都是再写lua武将的
DIY,没有人关心AI的lua。在我看来,与cpp相比,lua来实现DIY的武将优点是安装卸载
方便且不会影响cpp源代码的整洁,缺点是不能debug,通过设置断点来观察变量,在测试
的过程中比较辛苦,还有就是偏于与系统耦合的技能比较难实现。这两天把AI部分的lua
代码看了一遍清楚了,除了身份判断部分,基本清楚了它的原理。鉴于lua武将DIY的教程
和范例已经比较完善,这次我就把AI的编写简要的整理一下。附件中的内容是和这次教程
相关的代码、相关武将的卡牌图片以及本帖的PDF版本。附件中有“侠”三武将的lua和
AI,以及两个我DIY的娱乐武将的AI(娱乐武将用CPP实现的,由于有一个修改性别的技能
改变了底层的Player类,因此无法用lua完美实现,所以就不提供代码了,有兴趣的同学可
以和我交流)。
首先,AI的编写与lua技能的代码有很大关系,我用自己设计的两个武将和“侠”三武
将做了实验并且完成了这五个武将的AI。在编写“侠”三武将的AI时遇到了一些问题,后
来我把寒秋的代码优化了一下才解决了问题。附件里会有“侠”三武将的代码,一个是原版
的,一个是我改进过的,大家可以对比下。(并没有冒犯的意思,纯学术上的交流)所以算
是给广大lua的DIY爱好者们几条编程方面的建议吧。
1. 写代码要整齐,缩进量要层层递进,保证代码的易读性
2. 写触发技的时候,要清楚每个触发事件的原理,最好不要加入多余的触发事件,否
则可能引入异常的行为。当然,一般来说引入多余事件只是降低程序运行的效率罢
了,对于神杀影响应该不大。
3. 没有必要的时候不要重载can_trigger、enabled_at_play、enabled_at_response这些
函数。对于can_trigger这个函数,重载直接return true然后在on_trigger里判断玩
家是否拥有该触发技和使用默认的can_trigger基本是一样的(因为can_trigger本身
返回值就是触发者是否拥有该技能且触发者是否活着,貌似死人没法触发技能)。
4. 如果一个触发技可以在多个时间下触发,在写on_trigger函数的时候大家请按照触
发的事件来写条件分支语句,这样逻辑会更加清晰一些。
5. 尽量避免在视作技中使用askForCard这个函数,我来举个例子:如果一个视作技可
以把闪当作无中生有来使用,如果你不用askForCard,那么操作流程将是点击技能
按钮à选择一张闪à点击确定当无中生有打出;但是如果使用了askForCard并且你
装备了八卦阵,那么你点击技能按钮à系统提示你需要打出一张闪à使用八卦阵à
判红à直接打出一张无中生有。一张牌当无中生有和零张牌当无中生有,知道区别
了吧。
6. 在触发技一定要使用askForSkillInvoke(player,self:objectName(),data),这里要注意第
三个参数data,它包含触发事件的详细信息,一般卖血技的是damage的结构体、
无言那种是cardEffect结构体、烈弓是slashEffect结构体。这些data中的信息在写
AI的时候十分重要,因此务必要加上以便于AI中完成复杂的判断。我之前写AI就
是因为askForSkillInvoke(player,self:objectName())导致需要的变量总是空值,郁闷了
很久。
7. 触发技中askForCard之前最好加上askForSkillInvoke。对于人来说,如果不加
askForSkillInvoke,不想发动机能的时候直接点取消就行,加上了askForSkillInvoke
无非就是在提示是否使用XXX技能时多点一个确定罢了。但是对于AI的编写,如
果不加askForSkillInvoke,那么这个技能对应的AI就要写在的
SmartAI:askForCard里,有点像cpp中的系统耦合技,这对于代码的整洁还有AI的
移植是不利的。但是加上askForSkillInvoke后,我们就可以单独的对这个选择写一
个AI,消除了耦合。
OK,建议提完了,开始说AI的编写吧。首先,基本上所有的非锁定技的视作技和触发
技都可以独立新建一个lua文件编写,禁止技在smart-ai中的cardProhibit和slashProhibit里
编写,锁定技在中的damageIsEffective、slashIsAvailable、prohibitUseDirectly、
isCompulsoryView中编写,一些锁定技的响应(需要调用askForCard的)在smart-ai中的
askForCard里编写,卡牌转换类技能在smart-ai中的getSkillViewCard里编写。
我把AI技能的编写分为几类:
1. 将卡牌视为三国杀已有主动使用卡牌的视作技,比如双雄、连环、火计
2. 将卡牌视为三国杀已有卡牌的视作技,这些卡牌用于响应,比如倾国、看破、急救
注:武圣、龙胆、酒池这些技能既属于第一类也属于第二类
3. 将卡牌视为技能卡的视作技,比如青囊、离间、驱虎
4. 所有的非锁定技的触发技,比如刚烈、八阵、悲歌
5. 禁止技,比如空城、谦逊、帷幕
6. 使效果无效的技能,暂且称无效技,比如无言、智迟、神君;有威慑力的技能,比
如刚烈、恩怨、雷击
7. 视作锁定技,比如戒酒、武神
8. 对出杀次数、杀指定目标数、杀的距离修改的技能,比如天义、咆哮、方天画戟
9. 响应时的斟酌,比如AOE奸雄收牌、无双肉林只有一张闪就直接掉血
此外,一些技能还涉及到选项,比如志继觉醒摸牌还是回血、反间选择花色、无谋弃掉
标记还是流失体力;另一些技能涉及到目标的选择,比如驱虎赢了选择受伤害者、旋风选择
伤害者或杀的目标、眩惑来的牌给谁。这就要求我们具体问题具体分析了。
第一类技能编写的通式是:
local skillname_skill={}
skillname _=" skillname "
(_skills, skillname)
skillname _nUseCard=function(self)
判断是否满足应该使用的条件,若不满足则return nil
local card
选则要发动技能的卡牌,赋值给card
if not card then return nil end
local suit = card:getSuitString()
local number = card:getNumberString()
local card_id = card:getEffectiveId()
local card_str = ("转换卡牌名称:skillname[%s:%s]=%d"):format(suit, number, card_id)
local skillcard = _Parse(card_str)
assert(skillcard)
return skillcard
end
例子是以下这个武将的技能—求败:
["dgqb"] = "独孤求败",
["luawujian"] = "无剑",
["luayoujian"] = "有剑",
["luayoujiandev"] = "有剑",
["luaqiubai"] = "求败",
["luaqiubaix"] = "求败",
[":luawujian"] = "当你没有装备武器时你的杀不可被闪避。",
[":luaqiubai"] = "回合内你可以将任意武器牌当决斗使用。若决斗成功则你可以摸两张牌
否则对方摸两张牌",
[":luayoujian"] = "当你装备有武器时本回合你可以出X张杀,X为你当前的攻击范围,回
合内你使用的所有杀都将被计数(不包括决斗出的杀)。",
--luaqiubai
local luaqiubai_skill={}
--设置技能名称
luaqiubai_="luaqiubai"
--把这个技能添加进ai_skills列表中,使AI可以使用这个技能
(_skills,luaqiubai_skill)
--判断技能是否可以发动的函数
luaqiubai_nUseCard=function(self)
--获取手牌中杀的个数
local slash_num = self:getCardsNum("Slash")
--获取该回合出杀的次数
local slash_used = :usedTimes("Slash")
--如果已装备武器,且手中有杀多于一个,
--且获取该回合出杀的次数小于攻击范围,则不发动该技能
--这是为了保证有剑和求败两个技能利用最大化
if :getWeapon() and slash_num > 0
and slash_used < :getAttackRange() then return nil end
--获取我所有的手牌和装备牌
local cards = :getCards("he")
cards=2Table(cards)
local card
--根据使用价值来排序
self:sortByUseValue(cards,true)
--遍历所有卡牌
for _,acard in ipairs(cards) do
--如果是武器牌则使用这个卡牌
if acard:inherits("Weapon") then
card = acard
break
end
end
--如果没选发动技能的卡牌则返回空值
if not card then return nil end
local suit = card:getSuitString()
local number = card:getNumberString()
local card_id = card:getEffectiveId()
--构造卡牌字符串,卡牌名称:技能名称[花色:点数]=卡牌ID
local card_str = ("duel:luaqiubai[%s:%s]=%d"):format(suit, number, card_id)
--指定要使用的卡牌
local skillcard = _Parse(card_str)
assert(skillcard)
return skillcard
end
第二类技能编写是在中的getSkillViewCard中添加代码,用于响应的卡牌有
杀、闪、桃、酒、无懈,我们只需要在对应的分支语句里添加即可。比如在对闪的处理可以
看到龙魂、龙胆和倾国,如果我们加一个技能叫“我闪”,这个技能可以把装备牌当闪使用,
那么用下方红色的代码就能实现它的AI:
…………
elseif class_name == "Jink" then
if player:hasSkill("longhun") and player:getHp() <= 1 then
if card:getSuit() == _Club then
return ("jink:longhun[%s:%s]=%d"):format(suit, number, card_id)
end
end
if card_place ~= _Equip then
if player:hasSkill("longdan") and card:inherits("Slash") then
return ("jink:longdan[%s:%s]=%d"):format(suit, number, card_id)
elseif player:hasSkill("qingguo") and card:isBlack() then
return ("jink:qingguo[%s:%s]=%d"):format(suit, number, card_id)
end
end
if card_place == _Equip and player:hasSkill("woshan") then
return ("jink:woshan[%s:%s]=%d"):format(suit, number, card_id)
end
…………
有一个情况比较特殊,zeroCardView就是把0张牌当某牌,这个很特殊,目前只用来实
现酒诗的AI,前两种技能如果是把0张牌当某牌,AI都可以用这个函数实现。如果写一个
技能“万兽”和酒诗相似,只是正面向上时翻面可以当南蛮入侵,那么添加红色代码即可:
local function zeroCardView(class_name, player)
if class_name == "Analeptic" then
if player:hasSkill("jiushi") and player:faceUp() then
return ("analeptic:jiushi[no_suit:0]=.")
end
else if class_name == "SavageAssault" then
if player:hasSkill("wanshou") and player:faceUp() then
return ("savage_assault:wanshou[no_suit:0]=.")
end
end
end
第三类技能编写的通式是:
local skillname_skill={}
skillname _=" skillname "
(_skills, skillname)
skillname _nUseCard=function(self)
判断是否满足应该使用的条件,若不满足则return nil
local card
选则要发动技能的卡牌,赋值给card
if not card then return nil end
local card_str = ("@SkillNameCard=%d"):format(card:getId())
local skillcard = _Parse(card_str)
assert(skillcard)
return skillcard
end
_skill_use_func["SkillNameCard"]=function(card,use,self)
local target
选择技能发动的目标,若没找到合适目标则return nil
= card
=append(target)
return
end
例子是以下这个武将的技能—争宠:
["wujian"] = "吴健",
["zhengchong"] = "争宠",
[":zhengchong"] = "出牌阶段,你可以弃一张手牌,指定两名女性角色。若如此做,视为
其中一名女性角色对另一名女性角色使用一张【决斗】(此【决斗】不可被【无懈可击】响
应),每回合限用一次",
["yange"] = "阉割",
[":yange"] = "你每次对其他男性角色造成伤害,可使收到该伤害的角色变为女性,直到游
戏结束",
--zhengchong
local zhengchong_skill={}
--设置技能名称
zhengchong_="zhengchong"
--把这个技能添加进ai_skills列表中,使AI可以使用这个技能
(_skills,zhengchong_skill)
--判断技能是否可以发动的函数
zhengchong_nUseCard=function(self)
--如果该回合发动过则不发动
if :hasUsed("ZhengchongCard") then
return
end
--如果我有手牌或装备
if not :isNude() then
local card
local card_id
--如果我装备了白银狮子并且受伤了,那就用白银狮子发动技能
if self:isEquip("SilverLion") and :isWounded() then
card = _Parse("@ZhengchongCard=" .. :getArmor():getId())
--如果我没受伤或没装备狮子,且手牌数大于体力值
elseif :getHandcardNum() > :getHp() then
--获取我所有手牌
local cards = :getHandcards()
cards=2Table(cards)
--对所有手牌进行遍历
for _, acard in ipairs(cards) do
--如果是基本牌或装备牌或五谷丰登且不是桃和屎
if (acard:inherits("BasicCard") or acard:inherits("EquipCard") or
acard:inherits("AmazingGrace"))
and not acard:inherits("Peach") and not acard:inherits("Shit") then
--获取这张牌的ID
card_id = acard:getEffectiveId()
break
end
end
--如果我没受伤或没装备狮子,且手牌数不大于体力值,且有装备
elseif not :getEquips():isEmpty() then
local player=
--按照有武器、有进攻马、有防御马、有防具且手牌数不大于1的顺序弃
置来发动技能
if player:getWeapon() then card_id=player:getWeapon():getId()
elseif player:getOffensiveHorse() then
card_id=player:getOffensiveHorse():getId()
elseif player:getDefensiveHorse() then
card_id=player:getDefensiveHorse():getId()
elseif player:getArmor() and player:getHandcardNum()<=1 then
card_id=player:getArmor():getId()
end
end
--如果我没受伤或没装备狮子,且手牌数不大于体力值,且装备只有防具,且
手牌数大于1
if not card_id then
cards=2Table(:getHandcards())
--对所有手牌进行遍历
for _, acard in ipairs(cards) do
--如果是基本牌或装备牌或五谷丰登且不是桃和屎
if (acard:inherits("BasicCard") or acard:inherits("EquipCard") or
acard:inherits("AmazingGrace"))
and not acard:inherits("Peach") and not acard:inherits("Shit") then
card_id = acard:getEffectiveId()
break
end
end
end
--如果还没有选到发动技能的卡牌就不发动了否则返回技能卡牌字符串
if not card_id then
return nil
else
card = _Parse("@ZhengchongCard=" .. card_id)
return card
end
end
return nil
end
--判断技能卡牌该如何使用的函数
_skill_use_func["ZhengchongCard"]=function(card,use,self)
--一个技能内部使用的函数,用来返回杀最多的友方女性
local findFriend_maxSlash=function(self,first)
self:log("Looking for the friend!")
local maxSlash = 0
local friend_maxSlash
--遍历每个友方玩家,找到杀最多的那个女性
for _, friend in ipairs(s_noself) do
if (self:getCardsNum("Slash", friend)> maxSlash) and
friend:getGeneral():isFemale() then
maxSlash=self:getCardsNum("Slash", friend)
friend_maxSlash = friend
end
end
--如果找到了杀最多的友方女性
if friend_maxSlash then
--定义一个用来判断是否安全的布尔变量
local safe = false
--被决斗的人要是有刚烈、反馈、恩怨
if (first:hasSkill("ganglie") or first:hasSkill("fankui") or first:hasSkill("enyuan"))
then
--如果他们1血0手牌则判断为安全
if (first:getHp()<=1 and first:getHandcardNum()==0) then safe=true end
--若敌方没有刚烈、反馈、恩怨,且友方杀的数量大于敌方手牌数则判断
为安全
elseif (self:getCardsNum("Slash", friend_maxSlash) >=
first:getHandcardNum()) then safe=true end
--如果安全则返回杀最多的友方女性否则返回空值
if safe then return friend_maxSlash end
else
self:log("unfound")
end
return nil
end
--如果该回合没发动过该技能
if not :hasUsed("ZhengchongCard") then
--将敌人按照体力值排序
self:sort(s, "hp")
local females = {}
local first, second
local zhugeliang_kongcheng --改变两专门用于识别空城诸葛亮
--创建一个虚拟的决斗卡牌
local duel = sha:cloneCard("duel", _NoSuit, 0)
--遍历每个敌人
for _, enemy in ipairs(s) do
--如果敌人中有空城诸葛亮且已选被决斗者且空城诸葛亮决斗伤害对被
决斗者有效
if zhugeliang_kongcheng and #females==1 and
self:damageIsEffective(zhugeliang_kongcheng, Struct_Normal, females[1])
--则发起决斗者为空城诸葛亮
then (females, zhugeliang_kongcheng) end
--如果该敌人是女性且没有无言
if enemy:getGeneral():isFemale() and not enemy:hasSkill("wuyan") then
--如果该敌人有空城且没有手牌,则该敌人是空城诸葛亮
if enemy:hasSkill("kongcheng") and enemy:isKongcheng() then
zhugeliang_kongcheng=enemy
--如果该敌人不是诸葛亮
else
者
if #females == 0 and self:hasTrickEffective(duel, enemy) then
(females, enemy)
--如果发起决斗者还没定且决斗伤害对被决斗者有效,则该敌人
为发起决斗者
elseif #females == 1 and self:damageIsEffective(enemy,
Struct_Normal, females[1]) then (females, enemy) end
end
--如果被决斗者还没定且决斗对该敌人有效,则该敌人为被决斗
--如果决斗的两个人选好了就跳出循环
if #females >= 2 then break end
end
end
--如果在敌人中只找到了被决斗者,且友方不只有我一个人
if (#females==1) and #s_noself>0 then
self:log("Only 1")
first = females[1]
--如果敌方有空城诸葛亮且空城诸葛亮决斗伤害对被决斗者有效
if zhugeliang_kongcheng and self:damageIsEffective(zhugeliang_kongcheng,
Struct_Normal, females[1]) then
--则发起决斗者为诸葛亮
(females, zhugeliang_kongcheng)
--如果敌方没有空城诸葛亮或空城诸葛亮决斗伤害对被决斗者无效
else
--找出友方杀最多的女性
local friend_maxSlash = findFriend_maxSlash(self,first)
--如果友方杀最多的女性对被决斗者的决斗造成的伤害有效,则友方
杀最多的女性为发起决斗者
if friend_maxSlash and self:damageIsEffective(friend_maxSlash,
Struct_Normal, females[1]) then (females, friend_maxSlash) end
end
end
--如果决斗双方已选好
if (#females >= 2) then
first = females[1]
second = females[2]
--获取主公玩家
local lord = :getLord()
--如果被决斗者体力不大于1
if (first:getHp()<=1) then
--如果我是主公且身份预知被开启
if :isLord() or isRolePredictable() then
--找到友方杀最多的女性
local friend_maxSlash = findFriend_maxSlash(self,first)
--如果存在友方杀最多的女性,决斗发起者为友方杀最多的女性
if friend_maxSlash then second=friend_maxSlash end
--如果我不是主公或身份预知未开启,且主公是女性且没有无言
elseif (lord:getGeneral():isFemale()) and (not lord:hasSkill("wuyan"))
then
--如果我是反贼,且被决斗者不是主公,且主公对被决斗者的决
斗伤害有效,则决斗发起者为主公
if (=="rebel") and (not first:isLord()) and
self:damageIsEffective(lord, Struct_Normal, first) then
second = lord
--如果我不是反贼或被决斗者不是主公
else
--如果我是忠或内,且被决斗者没有刚烈、恩怨,且被决斗
者杀的数量小于决斗发起者,则决斗发起者为主公
if ((=="loyalist" or (=="renegade") and not
(first:hasSkill("ganglie") and first:hasSkill("enyuan"))))
and ( self:getCardsNum("Slash",
first)<=self:getCardsNum("Slash", second)) then
second = lord
end
end
end
end
--如果决斗双方不为空,则指定卡牌使用目标
if first and second then
if then
:append(first)
:append(second)
end
end
=card
end
end
end
话说这个技能的AI是我照离间的AI改的,其实就是把性别和变量名改了,其他的都没
动,确实考虑的相当周到,不过我感觉damageIsEffective这个函数貌似用错了。因为在
中函数定义是function SmartAI:damageIsEffective(player, nature, source),也就是
说第一个参数是被伤害者,第三个参数是伤害者,感觉在这个技能的AI中两者写反了。不
过对于绝大多数情况这个函数的返回值都是true,所以结果都一样。请专业编写AI的前辈
们看一下这里是不是写错了。
第四类技能编写的通式是:
_skill_ame = function(self, data)
判断是否触发技能,是则return true,否则return false
end
例子是以下这个武将的技能—销魂:
["yangguo"] = "杨过",
["luazhongjian"] = "重剑",
["luaxiaohun"] = "销魂",
[":luazhongjian"] = "当你使用杀时对方需使用一张闪加一张杀来闪避。",
[":luaxiaohun"] = "当你对女性或女性对你造成伤害时,被伤害者需判定,若结果为红色该
伤害无效否则该伤害+1。",
--判断触发技是否该触发的函数
_skill_ohun = function(self, data)
--如果友方有改判能力则发动
if self:hasWizard(s) then return true end
--如果敌方有改判能力则发动
if self:hasWizard(s) then return false end
--获取伤害者和受伤者
local damage = data:toDamage()
local from =
local to =
--如果我是伤害者,且受伤者体力小于2或伤害值大于1,则不发动
if from:objectName() == :objectName() and (to:getHp() < 2 or
> 1) then
return false
end
--如果我是受伤者,且我的体力大于2则不发动
if to:objectName() == :objectName() and :getHp() > 2 then
return false
end
--其他情况都发动
return true
end
这里需要说明一点,有些触发技的发动频率是Frequent,比如洛神、枭姬、英姿、连营,
一般来说我们不必对这类技能写AI。但是有些技能是例外,比如天妒和奸雄,如果是屎的
话就不要这个牌,这里多了一些判断,因此需要写AI。
以下代码就是奸雄的AI:
_skill_ong = function(self, data)
return not _HasShit(data:toCard())
end
第五类技能编写是在的cardProhibit和slashProhibit中添加技能AI的代码,
如果不添加这部分代码的话,lua写出来的禁止技只对人有效,AI照样无视你的技能。
杀以外其他卡牌的禁止技在cardProhibit中添加相应代码,比如写一个技能叫“吝啬”,
你不能成为过河拆桥的目标,那么就应该像红色代码这样写:
function SmartAI:cardProhibit(card, to)
if card:inherits("Slash") then return self:slashProhibit(card, to) end
if card:getTypeId() == _Trick then
if card:isBlack() and to:hasSkill("weimu") then return true end
if card:inherits("Indulgence") or card:inherits("Snatch") and to:hasSkill("qianxun")
then return true end
if card:inherits("Duel") and to:hasSkill("kongcheng") and to:isKongcheng() then
return true end
if card:inherits("Dismantlement") and to:hasSkill("linse") then return true end
end
return false
end
杀的禁止技要在slashProhibit中添加相应代码,比如实现一个技能“黑盾”,你不能成
为黑色杀的目标,那么就应该在slashProhibit函数中添加以下代码:
if card:isBlack() and self:hasSkill(“heidun”) then return true end
slashProhibit这个函数是用来让AI不把杀用在某目标身上,例如若只有一张杀是基本牌,
我就不会杀刘禅;我手牌少体力也少,那我就不会杀夏侯惇;我不会杀有明闪的敌方张角等。
也就是说,这个函数完成了对禁止技、无效技和威慑技的AI。
第六类技能的编写,对于杀的处理在slashProhibit里已经讲过了,建议大家认真读一下
这个函数。对于其他技能,比如无言,这个就比较麻烦,在每个锦囊的使用函数
useCardTrickName,判断AOE是否有效的函数aoeIsEffective,是否使用锦囊的函数
useTrickCard、判断锦囊是否有效hasTrickEffective、获取使用优先级的函数getUsePriority中
都有对它的考虑。从这里可以看出,要想把AI做好,全面的考虑是非常繁琐但是又必要的。
这里提供几个函数及其功能,对于这类技能大家可以在这些函数里添加代码:
slashIsEffective:判断杀是否有效,比如黑杀对于禁和仁王盾
damageIsEffective:判断伤害是否有效,比如非雷属性伤害对大雾
aoeIsEffective:判断AOE是否有效,比如无言、鸡肋、祸首、巨象、帷幕、藤甲
hasTrickEffective:判断锦囊是否有效,比如无言、智迟
第七类技能的编写,就是不能把某牌当作它原来用,只能当作锁定技锁定的牌来用,高
顺酒当杀,神关羽红桃当杀。这是通过中的prohibitUseDirectly和
isCompulsoryView来完成的。比如我写一个技能“好战”,所有的延时类锦囊锁定视为决斗,
那么它的AI就使用红色代码部分实现:
local function prohibitUseDirectly(card, player)
if player:hasSkill("jiejiu") then return card:inherits("Analeptic")
elseif player:hasSkill("wushen") then return card:getSuit() == _Heart
elseif player:hasSkill("ganran") then return card:getTypeId() == _Equip
elseif player:hasSkill(“haozhan”) then return card: inherits("DelayedTrick")
end
end
local function isCompulsoryView(card, class_name, player, card_place)
local suit = card:getSuitString()
local number = card:getNumberString()
local card_id = card:getEffectiveId()
if class_name == "Slash" and card_place ~= _Equip then
if player:hasSkill("wushen") and card:getSuit() == _Heart then return
("slash:wushen[%s:%s]=%d"):format(suit, number, card_id) end
if player:hasSkill("jiejiu") and card:inherits("Analeptic") then return
("slash:jiejiu[%s:%s]=%d"):format(suit, number, card_id) end
elseif class_name == “DelayedTrick” then
if player: hasSkill("haozhan") then return ("duel:haozhan[%s:%s]=%d"):format(suit,
number, card_id) end
end
end
第八类技能中对于出杀次数的限制是在的slashIsAvailable中添加代码:
例子是以下这个武将的技能—有剑,红色代码是这个技能的AI:
["dgqb"] = "独孤求败",
["luawujian"] = "无剑",
["luayoujian"] = "有剑",
["luayoujiandev"] = "有剑",
["luaqiubai"] = "求败",
["luaqiubaix"] = "求败",
[":luawujian"] = "当你没有装备武器时你的杀不可被闪避。",
[":luaqiubai"] = "回合内你可以将任意武器牌当决斗使用。若决斗成功则你可以摸两张牌
否则对方摸两张牌",
[":luayoujian"] = "当你装备有武器时本回合你可以出X张杀,X为你当前的攻击范围,回
合内你使用的所有杀都将被计数(不包括决斗出的杀)。",
function SmartAI:slashIsAvailable(player)
player = player or
if player:hasFlag("tianyi_failed") or player:hasFlag("xianzhen_failed") then return false end
if player:hasWeapon("crossbow") or player:hasSkill("paoxiao") then
return true
end
if player:hasSkill("luayoujian") and player:getWeapon() then
return (player:usedTimes("Slash") + player:usedTimes("FireSlash") +
player:usedTimes("ThunderSlash")) < player:getAttackRange()
end
if player:hasFlag("tianyi_success") then
return (player:usedTimes("Slash") + player:usedTimes("FireSlash") +
player:usedTimes("ThunderSlash")) < 2
else
return (player:usedTimes("Slash") + player:usedTimes("FireSlash") +
player:usedTimes("ThunderSlash")) < 1
end
end
对出杀距离和指定目标数的修改是在中的getTurnUse或useBasicCard中添
加相应代码,这里我就不举例了,大家看一下我下面列出的代码应该就会写了。两者有所不
同,像天义拼点成功是一种状态,这种状态与使用什么牌无关,所以在getTurnUse中有这
样一段代码:
if :hasFlag("tianyi_success") then
slashAvail = 2
_targets = 2
_distance_limit = true
end
而像武神无视距离出杀、方天最后一张牌三杀这些效果与卡牌有关,因此要写在
useBasicCard里,虎牢关神吕布第二形态的神戟我认为写在getTurnUse中是比较合理的,但
是实际上是在useBasicCard里:
if card:getSkillName() == "wushen" then no_distance = true end
if (:getHandcardNum() == 1
and :getHandcards():first():inherits("Slash")
and :getWeapon()
and :getWeapon():inherits("Halberd"))
or (:hasSkill("shenji") and not :getWeapon()) then
_targets = 3
end
第九类是响应事件的AI,这是在中的askForCard中实现,比如我对敌方的
法正造成了伤害,那我宁愿掉血也不会给他红桃的桃,吕布杀我而我只有一张闪,那我肯定
不出(别跟我提老诸葛为了空城、邓艾为了屯田可以出,我说的是普遍情况,当然如果把这
些都实现了,那AI真是太NB了,这个需要神杀AI的编写者们慢慢积累,不是一天两天能
达到的)。
例子是对以下这个武将的技能—重剑的响应:
["yangguo"] = "杨过",
["luazhongjian"] = "重剑",
["luaxiaohun"] = "销魂",
[":luazhongjian"] = "当你使用杀时对方需使用一张闪加一张杀来闪避。",
[":luaxiaohun"] = "当你对女性或女性对你造成伤害时,被伤害者需判定,若结果为红色该
伤害无效否则该伤害+1。",
红色代码部分是对响应这个技能添加的AI,可以看到下面还有对无双和肉林响应的AI
function SmartAI:askForCard(pattern, prompt, data)
…………
elseif pattern == "jink" then
--如果我没有杀,那我连第一张闪也不必出
if parsedPrompt[1] == "@luazhongjian-jink-1" and self:getCardsNum("Slash") < 1
then return "." end
if (parsedPrompt[1] == "@wushuang-jink-1" or parsedPrompt[1] ==
"@roulin1-jink-1" or parsedPrompt[1] == "@roulin2-jink-1")
and self:getCardsNum("Jink") < 2 then return "." end
…………
在一开始我们还说到有些技能需要在几个选项中做选择,或者是选择目标发动效果。
对于实现在几个选项中做选择的AI的通式是:
_skill_ame = function(self, choices)
if 选第1个选项的条件 then return “代表选项1的字符串”
elseif …………
elseif选第N个选项的条件then return “代表选项N的字符串”
end
return “默认选项”
end
对于实现选择目标发动效果的AI的通式是:
_skill_ame = function(self, targets)
local target
找到最适合发动效果的目标,为target赋值
return target
end
例子是凌统发动旋风时的选项和法正眩惑给牌对玩家的选择:
_skill_ng = function(self, choices)
--对敌人按防御强度排序
self:sort(s, "defense")
--创建一个虚拟的无属性杀
local slash = _Parse(("slash[%s:%s]"):format(_NoSuit, 0))
--遍历所有敌人
for _, enemy in ipairs(s) do
--若敌人与我距离不大于1,就选择对距离为1的人造成伤害
if :distanceTo(enemy)<=1 then
return "damage"
--若无属性杀对敌人有效,就选择无视距离出杀
elseif not self:slashProhibit(slash ,enemy) then
return "slash"
end
end
--否则选择什么都不做
return "nothing"
end
_skill_o = function(self, targets)
--遍历所有玩家
for _, player in (targets) do
--如果玩家手牌数不大于2或体力小于2,且他是我的友方
--且他不是被眩惑的人,则给他眩惑来的牌
if (player:getHandcardNum() <= 2 or player:getHp() < 2) and self:isFriend(player)
and not player:hasFlag("xuanhuo_target") then
return player
end
end
end
说完了如何编写AI,就该说一下对AI测试的方法,因为AI是用lua编写的,不能像cpp
那样有编译环境给你debug,因此经过反复的实验,我总结了一些可以当作debug的方法和
技巧:
1. 游戏打不开,提示lua第XXX行有错误:根据这个行数和文件改你的语法错误去吧。
2. 你写AI的那个武将不会主动出牌了,每回合就是摸牌弃牌:技能的AI里出问题了,
一般是调用了一个不存在的函数,或者是对空对象进行访问。这时你应该打开三国
杀程序à启动服务器à启动游戏à再做一次测试,在服务器的输出里会看到:
lua/ai/错误所在文件名.lua:XX行: attempt to call method '某函数' (a nil value)
根据这个线索我们就可以找到是哪里出错误了。
3. 有时测试为了判断AI是否跑进了某个条件语句,我们可以在那个条件语句里加入
以下代码,这个代码会在游戏的记录框(就是那个显示XX)里输出你给的字符串,
如果显示了正确的字符串,那么肯定跑进对应的条件语句里了。
local log = sage()
= 你想要输出的字符串
room:sendLog(log)
4. 视作技的测试方法:作弊获取足够你要测试的卡牌,或创造好视作技能够发动的条
件(我表示自定义小场景就是测试神器),然后托管看AI能不能发动技能。
5. 触发技的测试方法:人工操作手动触发,当弹出对话框提示是否发动技能时,托管
看AI的选择是否符合你的预期。
6. 无效技、威慑技、禁止技的测试方法:创造好你需要的条件,通常需要使用获取卡
牌拿顺拆把AI的手牌拆干净,然后你要测试的武将配刘备双将,把要测试的牌给
AI,然后弃牌到AI的回合看AI的表现。
7. 常用来配双将的测试型武将:刘备、二张、姜维,话说我还用cpp写了个能观看别
人手牌的锦囊,用来测试确实方便
终于把太阳神三国杀的CPP和LUA都研究完了,最后总结一下:
武将的DIY:我倾向用CPP实现,对于比较底层的修改(如卡牌的使用方法、攻击范围
的变化、新的触发事件)CPP有很大优势,因为CPP可以实现的比较完美,只要尽量避免耦
合,在原版的代码上修改时做好标记,保证代码的整洁就可以。
卡牌的DIY:貌似没见过用LUA来实现卡牌的,而且卡牌的实现比武将要底层得多,果
断CPP不解释。
AI的编写:虽然CPP可以写一些AI,而且我记得宇文天启有个帖子就是简单介绍这个
的,但是毕竟LUA已经非常完善并且是主流,所以果断LUA。
最后祝愿大家在DIY方面好点子越来越多,编程实现越来越顺利。本帖结束,OVER~
本文发布于:2024-08-12 12:03:54,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/1723435434297875.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |