太阳神三国杀LUA手册
初二 记叙文 32767字 456人浏览 骑上笨笨去拉萨

--大家好我是hypercross 。

--从这个文件开始讲解DIY 接口的用法。

--首先,这个文件说明DIY 需要的文件及其结构。

--DIY 是以module 的形式存在的。每个Module 即是一个UTF8格式的Lua 文件(建议用notepad++编辑),包含如下格式的代码:

module("extensions.moligaloo", package.seeall)

-- 进入module 。这里moligaloo 这个词必须和文件名相同。

extension = sgs.Package("moligaloo")

-- 创建扩展包对象。变量名必须为extension 。参数名为扩展包的objectName ,也是通常会使用的扩展包标识

shiqian = sgs.General(extension, "shiqian", "qun")

-- 创建武将对象, 这里我们的武将是时迁。关于武将属性的详细说明见reference 文档。

shentou = sgs.CreateViewAsSkill{

--创建技能,技能种类为ViewAsSkill 。这里的技能是“出牌阶段,你可以将任意一张梅花手牌当作顺手牵羊使用。”

name = "shentou",

n = 1,

view_filter = function(self, selected, to_select)

return to_select:getSuit() == sgs.Card_Club and not to_select:isEquipped()

end,

view_as = function(self, cards)

if #cards == 1 then

local card = cards[1]

local new_card =sgs.Sanguosha:cloneCard("snatch", card:getSuit(), card:getNumber())

new_card:addSubcard(card:getId())

new_card:setSkillName(self:objectName())

return new_card

end

end

}--关于技能的说明将是几乎所有其他帮助文件的重点。此处省略。

sgs.LoadTranslationTable{

["shentou"] = "神偷",

[":shentou"] = "你可以将你的梅花手牌当做顺手牵羊使用。",

}

--此段为翻译,将技能名称与描述中文化,否则你将会看到拼音

shiqian:addSkill(shentou)

--将神偷技能赋予时迁

--你可以将本文件保存至extension 目录下的moligaloo.lua 并启动游戏。此时扩展包即已经被添加至游戏。

--为了完善DIY 扩展包,需要将音频、图片以及翻译代码放到指定目录。这一点将在其他文档中说明。

--技能详解1:ViewAsSkill (视为技,也叫视作技)

--在太阳神三国杀中,常用的基本技能是两种:触发技和视为技。我们使用这两种基本技能的复合来完成大多数复杂的技能。

--此外,我们还有距离技,禁止技等特殊类型的技能,它们用于实现某些某种程度上 “要求改变游戏系统” 的技能。

--触发技可以用来实现”在某个时机,满足发动条件时,执行某个效果(包括做出选择)这样的技能。

--触发技也可以用来改变游戏事件而不仅是单纯的产生效果。比如,放弃摸牌阶段并执行xx 这样的技能也可以用触发技实现。

--事实上,一个触发技就是一条游戏规则。如果某名武将具有某个触发技,这个触发技就会被在服务端“注册”成为这局游戏的一条规则。

--被注册的触发技,通常会根据玩家“是否存活”以及玩家的武将“是否具有该技能”来决定是否被执行。

--视为技可以用来实现“可将某牌作为某牌打出”这样的技能。

--视为技的定义对于AI 而言是无效的。为了让AI 使用视为技,你基本上需要在AI 中重新写一遍技能的定义。

--这是因为视为技是在客户端运行的,而AI 在服务器端运行;触发技也是在服务端运行。 --如果某名玩家具有某个视作技,该技能通常就会被”注册“到该玩家的客户端。这个动作与其他玩家是不相干的。

--总之,视为技负责在客户端管理,有哪些牌可以选中,选中的牌又会被当成什么牌,这样的效果;

--但是视为技的运行本身不影响游戏进程;视为技做的事情是通过将特定的牌(甚至没有牌)“视为”特定的牌,

--来允许你使用那张本来不存在的牌。这也是大多数”主动技能“的实现方式。

--距离技和禁止技则分别用于实现“修改玩家间的距离”,以及“某人不能对某人使用某牌”这样的效果。

--与其他两种技能不同,距离技和禁止技不需要在某局游戏中注册:距离技是永远生效。

--也就是说,如果你写了一个距离技能叫”所有玩家间的距离-1“,那么无论场上有没有玩家具有这个技能,

--所有玩家间的距离都会被减一。禁止技也类似。

--这类技能的定义比较新,也比较清楚明了,可能你不需要看文档也能明白。

--正片开始

--首先讲解视为技。

--视为技在Lua 中的创建使用了sgs.CreateViewAsSkill 方法

-- **程序细节,以下可跳过

--sgs.CreateViewAsSkill 方法可以在lua\sgs_ex.lua中找到。

--该方法会创建一个LuaViewAsSkill 对象(定义见cpp 源码),然后将你定义的Lua 函数用于其成员函数。

--注意,本游戏cpp 源码部分使用了类的继承重载来实现不同的技能,而在lua 的DIY 技能里面我们都使用固定的类

--我们通过将会被用作技能成员函数的lua function ,直接赋给技能的实例来实现不同的技能。

-- **程序细节部分结束

--视为技在创建时,需要以下方法|变量的定义:

--name, n, view_filter, view_as, enabled_at_play和enabled_at_response

--name :

--一个字符串即技能名称。

--该字段必须定义。(无默认值)

--n :

--整数值,每次发动技能所用牌数的最大值。绝大多数DIY 用到的n 可能都为1或2. --默认值为0,即不需要选择自己的任何牌.

--view_filter:

--lua 函数,返回一个布尔值,即某张卡是否可被选中以用作发动技能。

--发动技能时,将对所有手牌、装备进行遍历,并执行view_filter方法。返回了true 的牌可以被选择用作技能发动。

--传入的参数为self(技能对象本身),selected(lua表,已经选择的所有牌), to_select(当前需判断是否可选中的牌)

--默认为" 永远返回false" ,即如果你没有定义,则这个技能的发动不允许你选择任何牌。

--view_as:

--lua 函数,返回一个card 对象,即被选中的牌应当被视为什么牌被打出或使用。 --这里的牌可以是游戏牌,也可以是技能牌。

--如果你的DIY 主动技能的效果不是某张游戏牌的效果,那么你需要把该效果定义到一个技能牌当中

--然后在view_as方法中得到并返回一张你定义的技能牌。

--传入参数为self(技能对象本身),cards(lua表,已经选择的所有牌)

--默认为" 返回空对象" ,即如果你没有定义,那么这个技能的发动永远不允许你点确定。

--enabled_at_play

--lua 函数,返回一个布尔值,即你在出牌阶段是否可以使用该技能。(该按钮是否可点) --传入参数为self(技能对象本身) ,player(玩家对象)

--默认为true ,即如果你没有定义,你永远可以在出牌阶段使用本技能。

--enabled_at_response

--lua 函数,返回一个布尔值,即你在需要用响应时,是否可以使用本技能进行响应。 --传入参数为self(技能对象本身) ,player(玩家对象),pattern(要求响应的牌的匹配规则) --默认为false, 即如果你没有定义,你永远不可以在响应时使用本技能。

--enabled_at_nullification

--lua 函数,返回一个布尔值,即你在询问无懈可击时是否可以使用该技能。(该按钮是否可点)

--传入参数为self(技能对象本身) ,player(玩家对象)

--默认为false ,即如果你没有定义,你永远不可以在出牌阶段使用本技能。

--** 实例

--以下为“任意一张草花牌”的view_filter方法:

n=1,

view_filter = function(self, selected, to_select)

return to_select:getSuit()==sgs.Card_Club

end,

--getSuit()返回一张牌的花色的enum 。

--如果to_select的花色为草花Club ,则返回真(可被选择)。否则返回假(不可被选择)。

--以下为“任意两张同花色手牌“的view_filter方法:

n=2,

view_filter = function(self, selected, to_select)

if #selected<1 then return not to_select:isEquipped() end

return to_select:getSuit()==selected[1]:getSuit() and not to_select:isEquipped()

end,

--如果选中的牌数小于1,那么任何未被装备的牌(手牌)都可以被选择;

--否则,只有那些和已被选中的第一张牌花色相同的牌才能被选中。

--以下为”当成借刀杀人使用“的view_as方法部分:

n=1,

view_as = function(self, cards)

if #cards<1 then return nil end

--如果没有牌被选中,那么返回空对象

local suit,number

for _,card in ipairs(cards) do

if suit and (suit~=card:getSuit()) then suit=sgs.Card_NoSuit else suit=card:getSuit() end if number and (number~=card:getNumber()) then number=-1 else number=card:getNumber() end

end

--如果所有被选中的牌的花色一样,那么被视为的借刀杀人的花色也为该花色。否则,该借刀杀人无花色。

--点数同理。

local view_as_card= sgs.Sanguosha:cloneCard("collateral", suit, number)

--生成一张借刀杀人

for _,card in ipairs(cards) do

view_as_card:addSubcard(card:getId())

end

--将被用作视为借刀杀人的牌都加入到借刀杀人的subcards 里

view_as_card:setSkillName(self:objectName())

--标记该借刀杀人是由本技能生成的

return view_as_card

end,

--将使用到的牌加入生成的借刀杀人的子牌列表中这一步,通常是必须的。

--因为可能存在对牌进行互动的技能,比如发动奸雄会得到发动乱击用的两张牌。

--将技能的名字即self:objectName赋予该卡的技能名属性,还是用于标记方便。

--需要其他游戏牌时,改动collateral 这个名字即可。需要根据不同种类、数量的牌得到不同游戏牌时,改动if #cards<1 then 这句即可。

--当技能的效果不能简单地描述为”视为你打出了xx 牌“时,你需要使用技能牌定义技能的效果。

--也就是说,将你的技能转述为”你可以将你的xx 牌作为xx 牌打出,其中xx 牌的效果为blahblah “。然后,在技能牌的定义中实现技能的效果。

--技能的效果用”技能牌“实现,技能的发动约束用”视为技“实现,技能的发动时机控制用”触发技“实现。

--以下是貂蝉离间技能的view_as方法:

view_as = function(self, cards)

if #cards<1 then return nil end

local view_as_card=lijian_Card:clone()

--lijian_Card的定义应该被包含在同一个module 文件当中。我将在其他文档中讲解技能牌的定义。

for _,card in ipairs(cards) do

view_as_card:addSubcard(card:getId())

end

return view_as_card

end,

--以下是周瑜“反间”的enable_at_play方法:

enabled_at_play=function(self,player)

return not player:hasFlag("fanjian_used")

--反间只能每回合用一次。如果你已经使用过了,那么你不能再次使用。 end

--注意在lua 中暂时不能使用player:hasUsed方法来判断是否用过你的某个LuaSkillCard --因为LuaSkillCard 并不使用在cpp 里面用的类继承机制

--请暂时在你的LuaSkillCard 中执行类似于room:setPlayerFlag(effect.from,"fanjian_used") --这样的语句,然后在enabled_at_play里面使用player:hasFlag。

--以下是大乔”流离“的enabled_at_response方法:

enabled_at_response=function()

return sgs.Self:getPattern()=="#liuli_effect"

--仅在询问流离时可以发动此技能

--这里的函数没有参数传入(我们不需要)

--我们直接调用了sgs.Self ,这个对象就是本地的ClientPlayer 对象。

--注意:只有在本地运行的代码(比如视作技)可以调用到sgs.Self 。

end

--虽然看起来流离并不像是一个视作技

--但事实上我们把它分成了这样的三部分:

--1、你成为杀的目标时,你可以响应使用" 流离牌" (技能牌)。用触发技实现。

--2、在且仅在你响应使用" 流离牌" 时,你可以将任意一张牌视为" 流离牌" 使用。用视为技实现。

--3、流离牌的效果为:流离牌的目标成为杀的目标,该杀跳过对你的结算。用技能牌实现。

--注意到正常情况下pattern 永远不会自己就是"#liuli_effect"

--你需要在触发技当中使用room:askForUseCard(daqiao,"#liuli_target",prompt)

--这样,就可以创造出一个专门用于流离的响应来。

--技能详解3:TriggerSkill 触发技

--许多技能都有”在xx 时机,如果xx 条件满足,那么执行xx 效果“这样的描述。

--由于游戏技时机繁多,许多技能也都是相互类似的,我们在游戏的cpp 部分有种类繁多的触发技定义方式。

--而基本的(也是万能的)触发技定义在Lua 中是使用sgs.CreateTriggerSkill 方法,该方法可以在lua\sgs_ex.lua中找到。

--CreateTriggerSkill 需要以下参数:

--name, frequency, events, on_trigger, can_trigger, priority

--name :

--技能名称字符串

--没有默认„„技能就是要有名字才行

--frequency :

--Frequency 枚举类型,技能的发动频率。

--执行askForSkillInvoke (询问技能发动)时,frequency 会影响玩家做决定的方式。 --frequency 也起到了技能分类以及用于增加技能提示显示的作用。

--frequency 可能的值有:

--sgs.Skill_Frequent(频繁发动:该技能会有一个可以打钩的按钮,如果勾选上,askForSkillInvoke 就不会弹出提示而是直接返回true )

--sgs.Skill_NotFrequent(不频繁发动:该技能的askForSkillInvoke 总是会弹出提示询问玩

家是否发动)

--sgs.Skill_Compulsory (锁定技:该技能的默认优先度为2而不是1;该技能会在显示上提示玩家这是一个锁定技能)

--sgs.Skill_Limited (限定技:该技能会在显示上提示玩家这是一个限定技能)

--ssg.Skill_Wake(觉醒技:该技能的默认优先度为2而不是1;该技能会在显示上提示玩家这是一个觉醒技)

--frequency 的默认值为sgs.Skill_NotFrequent

--events :

--Event 枚举类型,或者一个包含Event 枚举类型的lua 表。代表该技能的触发时机。 --可用的Event 列表请参考游戏代码中的struct.h 文件。

--无默认值。

--on_trigger:

--lua 函数,无返回值,执行事件触发时的技能效果。

--如果需要区分不同的事件执行不同效果,请根据event 参数使用条件语句。

--通常需要将事件数据(data)转为具体的游戏结构对象才能进行操作。你可以在源码的swig/qvariant.i文件中看到定义。

--on_trigger的传入参数为self(技能对象本身),event(当前触发的事件),player(事件触发者),data(事件数据)

--无默认值。

--can_trigger:

--lua 函数,返回一个布尔值,即是否能触发该技能。

--传入参数为self(技能对象),target(事件触发者)

--默认条件为“具有本技能并且存活”

--在这里个人只建议写简单的条件,许多判断都放在on_trigger里面做return 其实都是可以的

--priority:

--整数值,代表本技能的优先度。

--如果本技能与其他技能(或规则)在同一个时机都触发,那么优先度影响这些技能或规则的执行顺序。

--优先度更大的技能(或规则)优先执行。游戏规则的优先度为0,典型的技能优先度为1,而受到伤害发动的技能优先度通常为-1.

--锁定技和觉醒技的优先度默认为2,其他情况下默认为1

-- **实例:

--以下是曹操奸雄的实现:

jianxiong=sgs.CreateTriggerSkill{

frequency = sgs.Skill_NotFrequent,

name = "jianxiong",

events={sgs.Damaged}, --或者events=sgs.Damaged

on_trigger=function(self,event,player,data)

local room = player:getRoom() local card = data:toDamage().card

--这两步通常是必要的。我们需要room 对象来操作大多数的游戏要素,我们也需要将data 对象转成对应的数据类型来得到相应的信息。

if not room:obtainable(card,player) then return end

if room:askForSkillInvoke(player,"jianxiong") then

room:playSkillEffect("jianxiong")

player:obtainCard(card)

end

end

}

--在on_trigger方法中,我们首先获取了room 对象。

--对于影响整盘游戏的效果,我们必须需要获取room 对象。大多数情况下,room 对象都是必须获取的。

--on_trigger方法的data 参数是一个QVariant ,根据不同的事件我们需要用不同的方法得到它原本的数据类型。

--对于Damaged 事件(你受到了伤害),data 对象的类型是DamageStruct ,我们使用toDamage()得到DamageStruct 。

--询问技能发动时,需要使用room 对象的askForSkillInvoke 方法。

--playSkillEffect 方法则可以播放技能的发动效果。(但是对技能发动效果本身没有影响)

--player:obtainCard(card) 即让player 得到造成伤害的card 。

--在”某个阶段可触发“的技能,或者”摸牌时改为xx “这样的技能,可以使用PhaseChange 事件来触发,并对event 对象进行判断进行触发控制。

--对于在复数个时机发动的触发技,我们需要在on_trigger中使用条件语句。

--以下是袁术”庸肆“技能的实现:

yongsi=sgs.CreateTriggerSkill{

frequency = sgs.Skill_Compulsory, --锁定技

name = "yongsi",

events={sgs.DrawNCards,sgs.PhaseChange}, --两个触发时机

on_trigger=function(self,event,player,data)

local room=player:getRoom()

local getKingdoms=function() --可以在函数中定义函数,本函数返回存活势力的数目

local kingdoms={}

local kingdom_number=0

local players=room:getAlivePlayers()

for _,aplayer in sgs.qlist(players) do

if not kingdoms[aplayer:getKingdom()] then

kingdoms[aplayer:getKingdom()]=true

kingdom_number=kingdom_number+1

end

end

return kingdom_number

end

if event==sgs.DrawNCards then

--摸牌阶段,改变摸牌数

room:playSkillEffect("yongsi") data:setValue(data:toInt()+getKingdoms())

--DrawNCards 事件的data 是一个int 类型的QVariant 即摸牌数,改变该QVariant 对象会改变摸牌数

elseif (event==sgs.PhaseChange) and (player:getPhase()==sgs.Player_Discard) then

--进入弃牌阶段时,先执行庸肆弃牌,然后再执行常规弃牌

local x = getKingdoms()

local e = player:getEquips():length()+player:getHandcardNum()

if e>x then room:askForDiscard(player,"yongsi",x,false,true)

--要求玩家弃掉一些牌

-- 最后两个参数为”是否强制“以及”是否包含装备“

else

--如果玩家的牌未达到庸肆的弃牌数目,那么跳过询问全部弃掉

player:throwAllHandCards()

player:throwAllEquips()

end

end

end }

--技能讲解2:技能牌SkillCard

--神杀中,技能的效果在很多时候都技能牌实现。即,把技能定义在一张没有实体的抽象“牌”当中,当你发动技能的时候,视为你使用了这张牌。

--对于指定对象发动的技能,对象的指定也算在牌的效果当中。

--很多游戏的技能发动都带有cost 这个概念,即发动技能的代价。神杀中,cost 只能是你的牌或装备;也就是说,

--cost 只能靠ViewAsSkill 来实现。如果想实现类似于“发动代价”这样的效果,请用“发动技能的负面效果”这样的概念来替换。

--由于技能牌的需要有多个实例存在(每次发动技能得到一个技能牌),

--我们在DIY module当中并不像ViewAsSkill 和TriggerSkill 当中使用构造函数来创建SkillCard 。 --我们需要将SkillCard 的参数在一个lua table当中定义好,然后在每次需要创建SkillCard 的时候再调用sgs.CreateSkillCard 获取SkillCard 对象。

--或者,我们也可以先创建好一个SkillCard ,然后在技能中复制它。

--sgs.CreateSkillCard 需要以下参数定义:

--name,target_fixed,will_throw, filter,feasible,on_use,on_effect

--name:

--字符串,牌的名字。取个好听的名字~

--没有默认值。快去取名字„„

--target_fixed:

--布尔值,使用该牌时是否需要玩家指定目标。

--默认为false ,使用时你需要指定目标,然后点确定。

--will_throw:

--布尔值,该牌在使用后是否被弃置。还记得subCards 吗?

--对于拼点技能,请将will_throw设为false ,否则对方将看到你的牌之后再选择拼点牌。 --也可以将will_throw设为false, 然后使用room:throwCard(card)这个方法来灵活地控制如何将牌移动到弃牌区。

--默认值为true 。

--filter :

--lua 函数,返回一个布尔值,类似于ViewAsSkill 中的view_filter,但filter 方法的对象是玩家目标。

--你在使用牌时只能指定玩家为对象,不能直接指定玩家的某张牌为对象;

--比如过河拆桥,在神杀中,“选择对方的一张牌并弃置”是过河拆桥的效果,但过河拆桥的对象只有对方玩家。

--如果你确实需要“作为对象的牌”,请还是在了解游戏机制后自行发明解决方法„„

--传入参数为self,targets(已经选择的玩家),to_select(需要判断是否能选择的玩家)

--默认条件为“一名其他玩家”。

--feasible :

--lua 函数,返回一个布尔值,相当于viewasSkill 的view_as方法是否应该返回nil 。 --在viewAsSkill 中,我们可以无数次选中牌,直到返回了有意义的view_as再点确定,

--所以view_as返回了无意义的Nil 也无所谓;然而在SkillCard 当中,点确定的机会只有一次, --因此我们规定用feasible 来排除无效使用的情况。

--只有在feasible 返回了true 时,你才可以点确定。

--传入参数为self,targets(已经选择的玩家)

--默认条件为"target_fixed为true(不需要指定目标) ,或者选择了至少一名玩家为目标"

--on_use:

--lua 函数,无返回值,类似于on_trigger,执行使用效果。

--传入参数为self,room(游戏房间对象),source(使用者),targets(牌的使用目标)

--无默认。

--on_effect:

--lua 函数,无返回值,同样用于执行使用效果,但只定义对于某一个目标的效果。 --通常情况下你只需要写on_effect或者on_use当中的一个。

--如果是没有目标或者是目标特定的技能,使用on_use;

--如果是有几个目标执行相同或类似的效果,使用on_effect。

--如果是玩家指定的目标,还是使用on_effect。

--以下为“离间牌”的 feasible 以及filter 方法:

filter=function(self,targets,to_select)

if (not to_select:getGeneral():isMale()) or #targets>1 then return false

elseif to_select:hasSkill("kongcheng") and to_select:isKongcheng() and #targets==0 then return false

else return true end

--当已经选择了多于1名玩家时,不能选择其他玩家。

--空城诸葛不能选择。

end

feasible=function(self,targets)

return #targets==2

--只有选择了2名玩家时,才能使用牌

end

--on_use 和 on_effect 为牌的效果执行。他们的区别在于生效时机的不同。

--on_use在牌被使用时生效,而on_effect在牌在使用中对某一名目标进行结算时生效。 --因此,在不存在需要结算“一名玩家对另一名指定的玩家”的效果时,使用on_use实行效果即可;

--存在指定的目标时,则原则上应该使用on_effect。

--on_use和on_effect可以同时存在。牌效果进行结算时,先执行on_use,然后对每名目标执行on_effect

--以下为“雷击牌”的on_effect方法:

on_effect=function(self,effect)

--effect 为一个CardEffectStruct ,其from 和to 为player 对象,代表谁使用的牌对谁结算

local from=effect.from

local to =effect.to

local room=to:getRoom()

--sefEmotion 在玩家的头像上显示表情

room:setEmotion(to,"bad")

--进行判定时,首先创建“判定”对象。

--pattern 为一个正则表达式,由冒号隔开的三段分别匹配牌名、花色和点数 --good 的定义和之后的判定结果获取有关。

--原则上,之前的pattern 与判定牌匹配时,如果这种情况下执行的效果对于判定者来说是“好”的,

--那么good 应该是true 。

local judge=sgs.JudgeStruct()

judge.pattern=sgs.QRegExp("(.*):(spade):(.*)")

judge.good=false

judge.reason="leiji"

judge.who=to

--然后,让room 根据此判定对象进行判定。判定结果依然在judge 里面。

room:judge(judge)

--如果判定结果是一个“坏”结果,那么造成伤害 if judge.isBad() then

--和判定一样,造成伤害时先创建伤害struct, 然后交由room:damage执行 local damage=sgs.DamageStruct()

damage.card = nil

damage.damage = 2

damage.from = from

damage.to = to

damage.nature = sgs.DamageStruct_Thunder

room:damage(damage)

else

room:setEmotion(from,"bad")

end

end,

--以下为孙权“制衡”的on_use方法:

on_use=function(self,room,source,targets)

--self 代表技能牌本身。由于是将“任意张牌当成制衡牌打出”,

--因此弃置制衡牌就等于弃置所有用来发动制衡的牌,也即被制衡掉的牌。

room:throwCard(self)

--摸取相当于被用来发动制衡的牌的数目的牌。

--可以用self:getSubcards()来获取这些牌的QList 。

room:drawCards(source,self:getSubcards():length())

end,

--技能讲解4:基本技能类型的组合与复杂技能的实现

--大多数技能都不是单独使用ViewAsSkill 或者TriggerSkill 就可以实现的。复杂的技能往往需要结合多种基本技能。

--一个的技能的描述,往往可以分成3部分:发动的时机、发动代价、效果。

--对于发动时机的控制,是只有触发技可以实现的。触发技可以在任何“有定义的时机”时产生作用。然而,对于需要根据消耗牌来决定效果的那些技能,用触发技往往无法实现,也很难符合游戏流程。

--ViewAsSkill 可以非常完美地实现技能的发动流程。只有VIewAsSkill 可以细致地操纵玩家发动技能所付出的代价。然而,ViewAsSkill 本身并没有技能效果的实现,只能使用游戏牌作为技能效果,或者借助SkillCard 。

--SkillCard 本身并不是一个技能,但是只有他可以把技能发动用牌和技能效果联系起来。它对于ViewAsSkill 而言是必需的。

--1. 结合基本技能的方法:

--我们把技能效果定义在一个技能牌skill_card当中,技能牌的定义可以不从属于任何技能。然后,由于时机判断是整个技能的发动最先进行的一步,我们将ViewAsSkill 定义在TriggerSkill 当中。最后,我们把TriggerSkill 的发动效果设为“询问使用skill_card”,而ViewAsSkill 的效

果设为“你可以将特定牌作为skill_card使用或打出”。如此,即实现了“xxx 时,你可以使用xx 牌发动,执行xxx 效果。”

--以下为大乔流离技能的实现:

liuli_card=sgs.CreateSkillCard{

name="liuli_effect",

target_fixed=false,

will_throw=true,

filter=function(self,targets,to_select)

if #targets>0 then return false end

if to_select:hasFlag("slash_source") then return false end

local card_id=self:getSubcards()[1]

if sgs.Self:getWeapon() and sgs.Self:getWeapon():getId()==card_id then

return sgs.Self:distanceTo(to_select)<=1

end

return sgs.Self:canSlash(to_select,true)

end,

on_effect=function(self,effect)

effect.to:getRoom():setPlayerFlag(effect.to,"liuli_target")

end

}

liuli_viewAsSkill=sgs.CreateViewAsSkill{

name="liuli_viewAs",

n=1,

view_filter=function(self, selected, to_select)

return true

end,

view_as=function(self, cards)

if #cards==0 then return nil end

local aliuli_card=liuli_card:clone()

--使用之前创建的skillCard 的clone 方法来创建新的skillCard

aliuli_card:addSubcard(cards[1])

return aliuli_card

end,

enabled_at_play=function()

return false

end,

enabled_at_response=function()

return sgs.Self:getPattern()=="#liuli_effect"

--仅在询问流离时可以发动此技能

end

}

liuli_triggerSkill=sgs.CreateTriggerSkill{

name="liuli_main",

view_as_skill=liuli_viewAsSkill,

events={sgs.CardEffected},

on_trigger=function(self,event,player,data)

local room=player:getRoom()

local players=room:getOtherPlayers(player)

local effect=data:toCardEffect()

if effect.card:inherits("Slash") and (not player:isNude()) and room:alivePlayerCount()>2 then

local canInvoke

for _,aplayer in sgs.qlist(players) do

if player:canSlash(aplayer) then

canInvoke=true

end

end

if not canInvoke then return end

local prompt="#liuli_effect:"..effect.from:objectName()

room:setPlayerFlag(effect.from,"slash_source")

if room:askForUseCard(player,"#liuli_effect",prompt) then

room:output("ha?")

for _,aplayer in sgs.qlist(players) do

if aplayer:hasFlag("liuli_target") then

room:setPlayerFlag(effect.from,"-slash_source")

room:setPlayerFlag(aplayer,"-liuli_target")

effect.to=aplayer

room:cardEffect(effect)

return true

end

end

end

end

end,

}

sgs.LoadTranslationTable{

["liuli_main"] = "lua流离",

[":liuli_main"] = "和流离技能描述一摸一样",

["#liuli_effect"] = "和流离的询问字串一摸一样",

}

--翻译代码

--技能实现说明:

--此技能的实现分3个部分

--第一部分是流离牌,效果为“将目标标记为流离对象”

--第二部分是流离ViewAsSkill ,“每当你需要打出一张流离牌时,你可以将任意一张牌当作流离牌打出”

--第三部分是流离TriggerSkill ,“触发技,每当你成为杀的目标时,你可以打出一张流离牌,然后让具有流离标记的对象代你成为杀的目标”

--[[

大家好我是 William915。

从这个文件开始讲解AI 的编写方法。

三国杀的 AI 由太阳神上与 hypercross 共同编写。

之后经过他们及 donle ,宇文天启和本人的发展。

本文档针对的 AI 稳定版本为 V0.7 Patch 2(20120201,c11f5ace8)。

在论坛上可以看到反馈 AI 的问题的帖子远比反映程序的问题的帖子要多得多,这表明了 AI 的复杂性。

熟悉 AI 发展历史的朋友一定知道有那么一段时间 AI 常常会改好了这个又坏了那个, 这是因为影响 AI 表现的因素太多。

一言以蔽之,AI 编写与修改是牵一发而动全身的。

目前的 AI 架构还不完善,随着架构的逐步完善文档也会逐步的调整。

而架构的完善,需要你们和我们的共同努力。

也许您已经知道在开始编写 MOD 和 LUA 扩展之前需要一定的准备。

AI 也是如此,这些准备包括:

+ 熟悉三国杀本身的源代码(知道如何去检索自己需要的东西就够了),

包括sanguosha_wrap.cxx文件。

+ 熟悉 MOD 或 LUA 扩展的源代码并作适当修改(下面将具体介绍)。

可以说,没有源代码作参考是很难编写 AI 的。

+ 熟悉 smart-ai 中的各个函数的用法。(下面将具体介绍)

+ 阅读 lua/ai 文件夹下的各个 AI 文件以对 AI 编写形成一个大致的概念。

如果您对 AI 还不熟悉,又想比较快上手,那么最好的方法是参考已有的 AI 。

其实在编写 Lua 扩展的时候也是一样,照葫芦画瓢是最好的方法。

++ AI 需要做什么?

AI 所做的事情只有一件:作决定。

如果您想知道有哪些地方需要编写 AI ,只要想一下游戏过程中什么地方您需要作决定就行了。

已有的 AI 文件提供了大部分情况下作决定的策略。

因此大家只需要把精力集中在与扩展的武将的技能相关的决定上就可以了。

对于 MOD 的编写者,当然还需要为扩展的卡牌编写相应的策略。

随着下面介绍的逐步深入,相信大家对于 AI 的这一特点会有更深的体会。

在编写 AI 之前,还要知道:现在的 AI 是基于技能的,而不是基于武将的。

因此,我们不需要给姜维觉醒后获得的观星额外写 AI ,只要把诸葛亮的观星 AI 写好就行了。

++ 为 AI 而修改技能代码

实际上如果在写技能时没有为 AI 考虑的话,有一些技能的 AI 甚至根本无法编写。 因为 AI 所能获得的信息是很有限的。

最常用的传递信息给 AI 的方法是通过 data 参数。例如 src/package/thicket.cpp 第 124 行附近颂威的代码:

foreach(ServerPlayer *p, players){

QVariant who = QVariant::fromValue(p);

if(p->hasLordSkill("songwei") && player->askForSkillInvoke("songwei", who)){

...

对照 serverplayer.h 可以看到,askForSkillInvoke 里面的第 2 个参数就是 data , 这个 data 就是给 AI 用的。

上面的代码如果写成 lua ,则是这样:

]]

for _, p in sgs.qlist(players) do

local who = sgs.QVariant()

who:setValue(p)

if p:hasLordSkill("songwei") and player:askForSkillInvoke("songwei", who) then

-- ...

end

end

--正是因为在编写技能时传入了 data ,我们在 thicket-ai.lua 中才能根据 data 判断是否需要颂威(第 55 至 58 行)。

sgs.ai_skill_invoke.songwei = function(self, data)

local who = data:toPlayer() -- 将 data (QVariant 类型)转换为 ServerPlayer* 类型 return self:isFriend(who) -- 如果是对友方,则发动颂威

end

--[[

因此,在开始编写 AI 之前,请相应修改您的程序代码以便 AI 正常工作。

给 AI 传递数据的另外一个方法是通过 tag 。例子可见鬼才,不再赘述。

++ 如何载入自己写的 AI ?

方法很简单,只要找到您的扩展包的名字,例如为 extension 。

则只要在 lua/ai 文件夹下新增一个文件 extension-ai.lua 并把相应的 AI 代码放到这一文件内即可。

对于扩展包的名字,cpp 扩展应查找形如这样的代码:

ThicketPackage::ThicketPackage():Package("thicket")

而 lua 扩展则应根据第一行:]]

module("extensions.moligaloo", package.seeall)

--[[上面两个例子相应的 AI 文件名分别应该为 thicket-ai.lua 和 moligaloo-ai.lua

++ 万一需要修改已有的 AI 文件?

虽然这次 AI 架构的编写力求做到对所有的扩展都不需要修改 smart-ai.lua 。

但是有一些情况可能还是需要修改已有的 AI 文件。

例如有一个像奇才一样的技能,那么在目前的版本里只能通过修改 SmartAI.getDistanceLimit 来实现。

但是这并不意味着需要修改 smart-ai.lua 文件。

事实上,您只要在自己的 AI 文档里头重新写一遍 SmartAI.getDistanceLimit 就可以了。 这时原来的 getDistanceLimit 会被您所写的覆盖掉。

强烈不建议为了某个技能而直接修改已有的 AI 文件(包括但不限于 smart-ai.lua )。 直接修改已有的 AI 文件会使得以后新版本发布时您的 AI 的更新过程变得十分繁琐。 zombie_mode-ai.lua 提供了一个修改 SmartAI.useTrickCard 函数的实例。

++ Lua 基础知识

Lua 语言的基础知识可以通过查阅 manual.luaer.cn 获得。

下面重点介绍一些与 AI 编写关系比较紧密的和容易混淆的 Lua 知识。

如果你还没有编写过 AI ,可以先跳过这一部分。

首先要记住Lua 是大小写敏感的。SmartAI 跟 smartai 不是同一个东西。

点,冒号与方括号:

这是最容易混淆的地方之一。这三种符号都用于对 Lua 里头的表作索引。下面三种写法是等价的:]]

example:blah(foo),

example.blah(example, foo)

example["blah"](example, foo)

--[[

nil, false 与 0:

任何一个变量在初始化之前都是 nil 。当一个函数没有返回任何值的时候,返回值也是 nil 。

C 中的 NULL 在 LUA 中被映射为 nil 。

nil ,false 与 0 在 Lua 里头两两不等。]]

if a then blah end

--[[上面的代码当 a 为 nil 和 false 的时候,blah 不会执行。

但是当 a 为 0 的时候,blah 会被执行。

给熟悉 C 的朋友提个醒:

Lua 里头没有 switch ,没有 continue ,没有 goto ,请不要在代码里使用这些关键字。 Lua 里头没有函数重载的说法,以下两种写法是等价的:]]

function blah(a, b, c) end

blah = function(a, b, c) end

--因此如果有下面的代码:

function blah(a,b,c) blahblah end

function blah(a,b) ... end

--[[则相当于给全局变量 blah 赋了两次值。结果第一行代码没有任何作用,blahblah 也不会被执行。

这正是上面说的“万一需要修改已有的 AI 文件”部分的原理。

表(table )与列表(QList )

这是两个完全不同的类型,但是很容易混淆。Lua 所能直接处理的是前者,但是通过调用room 里头的函数获得的往往是后者。

两者的转换可以通过下面代码来进行,这种转换是单向的:]]

Table = sgs.QList2Table(QList)

--[[两者的差别列出如下:

表t 列表l

索引 t[i] l:at(i-1)

长度 #t l:length()

插入 table.insert(t,foo) l:append(foo)

迭代算子 ipairs(t) sgs.qlist(l)

++ AI 中用到的表

在 AI 编写中主要会用到两个表,一个是sgs ,该表含有大量由 SWIG 提供的成员函数,另外一个表是 SmartAI 。

在具体的 AI 代码中出现的 self ,实际上都是在操作 SmartAI 这个表。

sgs 表的常用元素:

sgs.Sanguosha 指向 Engine 对象

sgs.qlist(l) 是 QList 对象的迭代算子

sgs.reverse(t) 将一个表的元素反序,得到反序后新的表。

例如 t == {a, b, c} 则 sgs.reverse(t) == {c, b, a}

sgs.QList2Table(l) 将一个列表转换为表。

SmartAI 表的常用元素:

SmartAI.player 指向 AI 对应的 ServerPlayer 对象

SmartAI.room 指向 AI 所在的房间(Room 对象)

SmartAI.role 是 AI 对应身份的字符串

(主公为 "lord" ,忠臣为 "loyalist" ,反贼为 "rebel" ,内奸为 "renegade" )

SmartAI.friends_noself 是一个包含 AI 的所有友方 ServerPlayer 对象指针的表(不包括自身)

SmartAI.friends 是一个包含 AI 的所有友方 ServerPlayer 对象指针的表(包括自身) SmartAI.enemies 是一个包含 AI 的所有敌方 ServerPlayer 对象指针的表(包括自身)

这两个表的其它重要元素将在后续文档中介绍。

++ 调试 AI 的基本方法。

为了调试 AI ,您必须通过点击“启动服务器”然后点击“启动游戏”的方式来启动游戏,不能通过“单机启动”。

调试 AI 的基本方法是通过在服务器端输出信息。输出信息的基本方法有三种:]] self:log(message)

self.room:output(message)

self.room:writeToConsole(message)

--[[其中前两种是等价的。与第三种的区别在于,前两种仅当 config.ini 中有 DebugOutput=true 时才会输出。

后一种无论什么情况都会输出。这里的 message 是一个字符串。

加入以下代码,则可以了解函数被调用的过程。]]

self:log(debug.traceback())

-- 还有一个重要的用于调试的函数是 assert ,将在 15-Activate.lua 介绍。

--[[

smart-ai.lua 是整个 AI 中最先载入的脚本,也是 AI 系统的核心。

下面就让我们来看一下 smart-ai.lua 文件的组成。

++ 第一部分:初始化、取值、排序(第 1 至 572 行)

! 全局变量 version :当前 AI 的版本;

! SmartAI:initialize() :对 AI 进行初始化。

这些元素一般在编写 AI 时都不建议直接使用。

在后续的文档中,以感叹号开头表示。

相应的,在编写 AI 时需要直接用到的元素则用星号开头的行表示。

可以直接使用但是较少用到的则以问号开头的行表示。

另外有一些元素尽管曾经经常直接使用,但是在新的 AI 中已经很少使用,这类元素用 *? 开头表示。

在后续文档中,如果没有特别说明,“玩家”一词指的是某个 ServerPlayer 对象。

? sgs.getValue(player):获得一个玩家的手牌数 + 血量 * 2(下称为综合值)。

? sgs.getDefense(player):获得一个玩家的防御值。

! SmartAI:assignKeep(num, start):获得 AI 留在手上不使用的牌。

? SmartAI:getKeepValue(card, kept):获得一张卡牌的保留值。

该函数将会载入表 sgs.ai_keep_value, sgs.[[武将名]]_keep_value 和 sgs.[[武将名]]_suit_value * sgs.ai_keep_value:包含卡牌的保留值的表,元素名为卡牌的类名(className )。 例子可见 maneuvering-ai.lua 第 96 行。]]

sgs.ai_keep_value.Analeptic = 4.5

--[[更多例子可参考旧版 AI 的 general_config.lua 文件的前 20 行。

*? sgs.[[武将名]]_keep_value:包含武将的卡牌的保留值的表,目前主要在 standard-ai.lua 中使用。

该表的元素名为卡牌的类名,例子如 standard-ai.lua 里面 586 行附近张飞的表:]] sgs.zhangfei_keep_value =

{

Peach = 6,

Analeptic = 5.8,

Jink = 5.7,

FireSlash = 5.6,

Slash = 5.4,

ThunderSlash = 5.5,

ExNihilo = 4.7

} --[[更多例子可以参考旧版 AI 的 value_config.lua 文件的第 57 行至第 152 行。

* sgs.[[武将名]]_suit_value:与上面类似。

不过这里的元素名是卡牌的花色名称(全部小写)

god-ai.lua 第 423 行提供了一个例子:]]

sgs.shenzhaoyun_suit_value =

{

heart = 6.7, --红桃

spade = 5, --黑桃

club = 4.2, --草花

diamond = 3.9, --方片

}

--[[ 更多例子可以参考 value_config.lua 文件的第 154 行至第 230 行。

? SmartAI:getUseValue(card):获得一张卡牌的使用价值。

该函数会载入表 sgs.ai_use_value。

* sgs.ai_use_value:包含卡牌的使用价值的表。例子如 standard_cards-ai.lua 第 609 行:]] sgs.ai_use_value.ExNihilo = 10 --无中生有

--[[更多例子可以参考 general_config.lua 文件的第 22 行至第 78 行。

? SmartAI:getUsePriority(card):获得一张卡牌的使用优先级。

该函数会载入表 sgs.ai_use_priority。

* sgs.ai_use_priority:一个包含卡牌的使用优先级的表。

元素名称为卡牌的类名,数值越大优先级越高。例子如 yjcm-ai.lua 第 114 行:]] sgs.ai_use_priority.XinzhanCard = 9.2

--[[更多例子可以参考 general_config.lua 文件的第 80 行至第 167 行。

? SmartAI:getDynamicUsePriority(card):获得一张卡牌的动态使用优先级。

用 getUsePriority 得到的优先级没有考虑到其它可能使用的卡牌。

事实上一张牌的优先级不是一成不变的。

手牌中拥有某些牌可能会使一张牌的使用优先级上升或者下降。

该函数会载入表 sgs.dynamic_value。

* sgs.dynamic_value:包含卡牌类型的表,这些类型用于调整其它卡牌的优先级。

该表包括以下几个子表,各子表的元素名称均为卡牌的类名,值则只取 true 。

damage_card:能造成伤害的牌

control_usecard:类似乐不思蜀的延时类锦囊

control_card:不直接造成伤害但是能造成负面效果的牌,如过河拆桥

lucky_chance:类似闪电的延时类锦囊

benefit :能造成正面效果的牌,如桃。

例子可见 maneuvering-ai.lua 第 133 行:]]

sgs.dynamic_value.control_usecard.SupplyShortage = true

--[[更多例子可以参考 value_config.lua 前 55 行。

从上面的例子我们可以看到 AI 运作的基本模式是载入适当的表。

而扩展编写者的任务就是为这些表设置适当的值,而不用去干预 AI 载入这些表的具体过程。

? SmartAI:cardNeed(card) 获得一张卡牌的需要程度。

* sgs.ai_chaofeng:包括武将嘲讽值的表。AI 倾向于集火嘲讽值高的对象。

在目前的 AI 中,嘲讽值起到的作用有限,例如不能直接影响 AI 出杀的决策。 但是在很多情况下,嘲讽值还是能发挥其作用的。

该表的元素名为武将名。例子如 god-ai.lua 第 53 行。]]

sgs.ai_chaofeng.shenguanyu = -6

--[[嘲讽值的默认值为 0。更多的例子可以参考 general_config.lua 的 172 行至 236 行。

AI 的一个重要任务是寻找最佳决策,因此对玩家或卡牌进行排序显得尤为重要。

smart-ai.lua 中提供了一系列对对象进行排序的函数。这些函数在 AI 编写中是经常用到的。

! sgs.ai_compare_funcs:包含对两个玩家进行比较的一系列函数的表,该表被 SmartAI:sort 调用。

* SmartAI:sort(players, key, inverse) :对表 players 中的玩家按照关键字 key 进行排序。 inverse 参数若存在且为 true ,表示排序之后要对整个表反序。

可用的关键字有:

* hp:按体力值从小到大排序,例如在 fire-ai.lua 第 138 行与典韦强袭有关的代码:]] self:sort(self.enemies, "hp") -- 将所有敌方玩家按体力值从小到大排序

--[[这里,如果写成self:sort(self.enemies, "hp", true),则表示将敌方玩家按体力值从大到小排序。

* handcard: 按手牌数从小到大排序

* value: 按综合值从小到大排序。

* chaofeng:按嘲讽值从大到小排序,嘲讽值相同的,则按综合值从小到大排序。 * defense:按防御值从小到大排序

* threat:按威胁值从大到小排序

下面是一些对卡牌排序的函数,其中 inverse 参数的含义与 SmartAI:sort 相同,不再赘述。 * SmartAI:sortByKeepValue(cards, inverse, kept):

在玩家已经决定保留 kept 中的牌的前提下,将表 cards 中的牌按保留值从小到大排序。 在实际使用中,很少用到 kept 参数。

例子如 standard-ai.lua 第 1081 至 1086 行与华佗青囊有关的代码:]]

local cards = self.player:getHandcards() -- 获得所有手牌

cards=sgs.QList2Table(cards) -- 将列表转换为表

self:sortByKeepValue(cards) -- 按保留值排序

local card_str = ("@QingnangCard=%d"):format(cards[1]:getId())

-- 用排序后的第一张(保留值最小)的卡牌作为青囊的卡牌。

--[[

* SmartAI:sortByUseValue(cards, inverse):将表 cards 中的牌按照使用价值从大到小排序。 * SmartAI:sortByUsePriority(cards, inverse) :将表 cards 中的牌按照使用优先级从大到小排序。

* SmartAI:sortByDynamicUsePriority(cards, inverse) :将表 cards 中的牌按照动态优先级从大到小排序。

? SmartAI:sortByCardNeed(cards):将表 cards 中的牌按照需要程度从小到大排序。

++ 第二部分:身份、动机、仇恨值(第 565 至 1225 行)

这一部分将在 16-RoleJudgement.lua 中详细介绍。对于一般的 AI 编写,所需要知道的函数是下面这些:

* SmartAI:isFriend(other, another):判断友善的关系。参数均为 ServerPlayer* 类型。

如果只有一个参数 other ,判断 other 是否自己的友方,如果是则返回 true ,否则返回 false 。 如果有两个参数,判断 other 与 another 之间是否互为友方。

* SmartAI:isEnemy(other, another):判断敌意的关系。与 SmartAI:isFriend 类似,不再赘述。 * SmartAI:getFriends(player):返回一个包含 player 的所有友方玩家对象指针的表。 * SmartAI:getEnemies(player):返回一个包含 player 的所有敌方玩家对象指针的表。 关于 SmartAI.isFriend 的例子,见 11-Fundamentals.lua 里面关于颂威代码的说明。

++ 第三部分:响应请求(第 1227 至 1910 行)

这一部分包括所有响应 Room::askFor*** 请求的函数。将在 14-Responsing.lua 详细介绍。 这一部分引入了一个重要的辅助函数:

? SmartAI:getCardRandomly(who, flags):

在 ServerPlayer* 指针 who 指向的玩家(在下面中简称为玩家who )的由 flags 标志的牌中,随机选择一张牌,返回其 ID 。

flags: 为字母 h ,e ,j 的任意组合。这三个字母分别表示手牌,装备区和判定区。

例如 self:getCardRandomly(sunshangxiang, "e") 表示在孙尚香的装备区里随便选择一张牌返回其 ID 。

(当然,在这句代码之前需要先定义 sunshangxiang 这个变量,例如通过下面的代码。 在后续的例子中均假设这样的代码已经存在,不再重复。)]]

local sunshangxiang = self.room:findPlayerBySkillName("xiaoji")

--[[

++ 第四部分:主动出牌(第 1912 至 2802 行)

这一部分主要包括出牌阶段主动出牌的相关策略,将在 15-Activate.lua 详细介绍。 下面先对这一部分引入的众多辅助函数进行说明。

下面为简洁起见,用 [] 注出可以缺省的参数,没有特殊说明时,player 缺省值均为 self.player 。

* SmartAI:getOverflow([player]):获得玩家 player 的手牌超额,即手牌数与手牌上限的差值。 返回值非负,返回值为 0 表示手牌没有超出手牌上限。

* SmartAI:isWeak([player]):判断玩家 player 是否处于虚弱状态。

目前虚弱状态定义为体力值不高于 1,或体力值不高于 2 且手牌数不高于 2。

? sgs.getSkillLists(player):获得玩家 player 的所有 ViewAsSkill 和 FilterSkill 的表。 下面的代码:]]

vsnlist, fsnlist = sgs.getSkillLists(player)

--[=[执行之后,vsnlist 为包含 player 的所有 ViewAsSkill 和 FilterSkill 的技能名(字符串)的表,

fsnlist 为包含 player 的所有 FilterSkill 的技能名的表。

* SmartAI:hasWizard(players[, onlyharm]):判断 players 中是否有能从判定中得益的玩家。 若 onlyharm 为 true ,则当 players 中有能改判的玩家时,返回 true ,否则返回 false 。 若 onlyharm 为 false 或省略,则当 players 中有能改判的玩家或有郭嘉时,返回 true ,否则返回 false 。

* SmartAI:needRetrial(judge):判断由 judge (为 JudgeStruct* 类型)指定的判定结果是否需要改判。

如果需要改判,则返回 true ,否则返回 false 。

* SmartAI:getRetrialCardId(cards, judge):

从表 cards 中选出适宜用于改判 judge 的判定结果的牌,返回其 ID 。

若找不到可以改判的牌,则返回 -1。注意本函数并不检验是否需要改判。

* SmartAI:damageIsEffective([player[, nature[, source]]]):

判断玩家 source 对玩家 player 造成的 nature 属性的伤害是否有效。

nature 为 sgs.DamgeStruct_Normal, sgs.DamageStruct_Thunder, sgs.DamageStruct_Fire 之一, 分别表示无属性、雷属性和火属性。

三个参数均可缺省。缺省时 player 和 source 为 self.player , nature 为 sgs.DamageStruct_Normal。

* SmartAI:getMaxCard([player]):获得玩家 player 的手牌中点数最大的一张。

* SmartAI:getCardId(class_name[, player]):

在玩家 player 的手牌与装备区中获得一张类名为class_name的牌,返回其 ID 。

* SmartAI:getCard(class_name[, player]):与 getCardId 一样,但是返回的是卡牌本身而不是其 ID 。

* SmartAI:getCards(class_name[, player[, flag]]):与 getCard 一样,

但是获得的不是一张卡牌而是所有符合条件的卡牌的表。

其中 flags 的含义与 SmartAI.getCardRandomly 相同。

* SmartAI:getCardsNum(class_name[, player[, flag[, selfonly]]]):

与 getCards 一样,但不是返回表本身而是返回表长(即牌数)。

selfonly 表示是否需要考虑房间里的其它玩家,

当 selfonly 为 false 或缺省时,有两种情况会计入其它玩家的牌数:

. player 有激将技能且计算【杀】的张数时,会计入所有友方蜀将的【杀】;

. player 有护驾技能且计算【闪】的张数时,会计入所有友方魏将的【闪】;

* SmartAI:getAllPeachNum([player]):获得玩家 player 及其友方所有的【桃】数。

在这一组函数中,全部均已经考虑视为技,但是需要编写相关的代码来使 AI 会使用视为技,详见 13-ViewAs.lua 。

* SmartAI:hasSuit(suit_strings[, include_equip[, player]]):

判断玩家 player 是否有由 suit_strings 指定的花色的手牌。

include_equip 为 true 时,同时计入装备区的牌。

suit_string 为 "spade", "heart", "club", "diamond" 的任意组合,

组合时以竖线分隔,例如 spade|club 表示黑色牌。

* SmartAI:getSuitNum(suit_strings[, include_equip[, player]]):

与 hasSuit 类似,但是返回的不是“有没有”而是“有多少张”。

* SmartAI:hasSkill(skill):判断自己是否有名称为 skill 的技能。

* SmartAI:hasSkills(skill_names[, player]):判断玩家 player 是否有由字符串 skill_names 所指定的一系列技能。

skill_names 为技能名的任意组合,分隔符为竖线。smart-ai.lua 2030 至 2034 行有 skill_names 的例子。]=]

sgs.lose_equip_skill = "xiaoji|xuanfeng"

sgs.need_kongcheng = "lianying|kongcheng"

sgs.masochism_skill = "fankui|jieming|yiji|ganglie|enyuan|fangzhu"

sgs.wizard_skill = "guicai|guidao|tiandu"

sgs.wizard_harm_skill = "guicai|guidao"

--说到这里,顺便提一下这种字符串与 table 之间的转换,用下面一个例子来说明再合适不过了。若

s == "foo|bar|any", t == {"foo", "bar", "any"}

--则

table.concat(t, "|") == s -- 再插一句,对于 QList ,相应的函数为 join 。

s:split("|") == t

--[[

* SmartAI:exclude(players, card):从玩家表 players 中剔除卡牌 card 不能用在其上的玩家。 例如]]

players == {jiaxu, sunshangxiang, wolong}

-- 且 card 为黑色过河拆桥,则在执行 SmartAI:exclude(players, card) 之后,

players == {sunshangxiang, wolong}

--[[

* SmartAI:trickProhibit(card, to):判断卡牌 card 能否对 to 使用。

* SmartAI:hasTrickEffective(card, player):判断卡牌 card 是否对玩家 player 有效。

这三个函数的区别是:前两个考虑的是是否存在相应的禁止技,后一个则是考虑实际上有没有效。

这一区别就如帷幕和智迟的区别。

* SmartAI:hasEquip(card):判断装备区是否有卡牌 card 。

* SmartAI:isEquip(equip_name, player):判断 player 是否装备有类名为 equip_name 的卡牌。(已经考虑八阵)

* SmartAI:hasSameEquip(card, player):判断 player 的装备区是否有与 card 卡牌属于同一种类的装备。

这里的“种类”是指武器、防具、防御马、进攻马之一。

++ 第五部分:载入扩展的 AI (第 2804 至 2830 行)

对于这一部分,需要知道的是,standard-ai.lua, standard_cards-ai.lua 和 maneuvering-ai.lua 总是在其它扩展之前载入,

因此在扩展的 AI 中,可以直接使用这三个文件中已经定义的元素。

除了 smart-ai.lua 之外,在 standard_cards-ai.lua 里头还有几个编写 AI 时经常用到的函数,介绍如下:

* SmartAI:slashProhibit(card, enemy):从策略上判断是否不宜对玩家 enemy 使用【杀】card 。 * SmartAI:slashIsEffective(card, enemy):判断【杀】card 是否对 enemy 有效

(无效是指诸如智迟、大雾之类的技能)。

* SmartAI:slashIsAvailable([player]):判断玩家 player 本回合还能否继续出【杀】。]]

--[[

下面介绍一下视为技的 AI 写法。事实上在这个文档里只会介绍视为技 AI 的一小部分。 对于与技能卡相关的视为技,以及出牌阶段的主动视为技,将在后续部分介绍。

之所以把这部分的视为技 AI 放到前面来讲,是因为这部分的 AI 比较简单。

基本上只是把技能换种方式重新写一遍,不涉及任何决策的问题。

需要把技能在 AI 里头重新写一遍,是因为视为技的代码在客户端执行,而 AI 则是在服务器端执行的。

这部分里介绍的内容与 SmartAI:getCard... 的一系列函数有关,具体包括哪些函数见 12-SmartAI.lua 。

视为技是广义的说法,实际上包括所谓的“锁定视为技” FilterSkill ,与一般视为技 ViewAsSkill 。

下面举例说明一下一些相关的表与其元素。

* sgs.ai_filterskill_filter:与锁定视为技有关的表。

例子如 god-ai.lua 第 32 至 37 行关于武神的代码:]]

sgs.ai_filterskill_filter.wushen = function(card, card_place) -- 武神技能的锁定视为技 local suit = card:getSuitString() -- 获得卡牌花色

local number = card:getNumberString() -- 获得卡牌点数

local card_id = card:getEffectiveId() -- 获得卡牌的有效 ID

if card:getSuit() == sgs.Card_Heart then return ("slash:wushen[%s:%s]=%d"):format(suit, number, card_id) end

-- 如果卡牌的花色为红桃,则返回武神杀,花色与点数不变。

end

--[[

从上面的例子可以看到,我们需要对这个表进行的操作是给表里的一个以技能名命名的元素赋值。

所赋的值为一个函数,函数原型为 function(card, card_place)

其中 card 表示需要处理的卡牌,而 card_place 表示卡牌所处的位置。

card_place 为 sgs.Player_Hand 或 sgs.Player_Equip,分别表示手牌与装备区。

返回值则为一个将会传递给 sgs.Card_Parse 函数的字符串。

该字符串经 sgs.Card_Parse 函数处理之后得到实际的卡牌。

因此,在这里有必要介绍两个重要的函数。

* sgs.Card_Parse,这个函数实际上就是源码里头的 Card::Parse,这一函数的原型如下: static const Card* Card::Parse(const QString &str);

对于 AI 编写来说,只要知道这个函数的传入参数是一个字符串,而返回值是相应的卡牌就可以了。

在 AI 编写中遇到的传入字符串主要有以下几种类型:

. 一个整数 n ,得到的卡牌是 ID 为 n 的卡牌。

例如 sgs.Card_Parse("0") 和 sgs.Card_Parse(0) 都得到黑桃 7 的【杀】。

后一种情况下,0 被 Lua 自动转型为字符串。

. 形如 "%object_name:%skill_name[%suit:%number]=%ids" 的字符串,

注意整个字符串中没有多余的空格。

返回一张虚拟卡,其对象名(objectName )为 %object_name,技能名为 %skill_name, 花色由 %suit (如 "spade", "no_suit")确定,点数由 %number 确定(如 "0", "A", "10", "K") %ids 是一串用加号连接起来的 ID ,表示该虚拟卡的全部子卡的 ID ,也可以为 "." ,表示该卡没有子卡。

例如 ]]

sgs.Card_Parse("archery_attack:luanji[diamond:K]=29+28")

--[[将得到一张万箭齐发的虚拟卡。

该卡为方片 K ,技能名为乱击,包含两张子卡,ID 分别为 28 (方片 10 的【杀】)与29(方片 K 的【杀】)。

. 形如 "@%class_name=%ids" 的字符串,同样在整个字符串中没有多余的空格。 返回一张技能卡,其类名为 %class_name,%ids 的含义与前面相同。

例如]]

sgs.Card_Parse("@RendeCard=0") --[[得到一张仁德技能卡,其子卡为黑桃 7 的【杀】。

. 对于 Lua 技能卡,相应的字符串应该替换为 "#%object_name:%ids"。

%object_name 为 Lua 技能卡的对象名,即 4-SkillCard.lua 第 16 行提到的 name 参数。

例如在已经定义了 5-Applications.lua 中提到的离间技能卡的前提下,]]

sgs.Card_Parse("#liuli_effect:0")

--[[将得到一张 Lua 流离技能卡,包括一张子卡,子卡为黑桃 7 的【杀】。

事实上,不仅仅是编写 AI ,在编写 Lua 扩展时恰当运用 sgs.Card_Parse,能使代码更为精简。

* string:format(...):字符串的格式化。

这个函数的功能是将给定的字符串中的参数占位符用相应的参数依次代入得到新的字符串。 所谓参数占位符,是指一些特殊的字符串,在 AI 中用到的主要有两种:"%s" 和 "%d"。 "%s" 表示字符串类型的参数,"%d" 表示数值(number )类型的参数。

上面说的还是比较抽象,下面看几个例子:]]

("%d + %d = %d"):format(2, 5 ,7) == "2 + 5 = 7" --注意,写成"%d + %d = %d":format(2, 5, 7)会出错。

("%d + %s = %s"):format(2, "5", "7") == "2 + 5 = 7"

("archery_attack:luanji[%s:%s]=%d+%d"):format("diamond", "K", 29, 28) ==

"archery_attack:luanji[diamond:K]=29+28"

--[[介绍完这两个重要的函数之后,让我们回到与视为技有关的表。

* sgs.ai_view_as:与一般视为技有关的表。

这个表与 sgs.ai_filterskill_filter 十分相似。其元素名也是技能名,元素也是函数。 例子可见 standard-ai.lua 197 至 204 行关于倾国的代码。]]

sgs.ai_view_as.qingguo = function(card, player, card_place)

local suit = card:getSuitString()

local number = card:getNumberString()

local card_id = card:getEffectiveId()

if card:isBlack() and card_place ~= sgs.Player_Equip then -- 如果是黑色牌且不在装备区 return ("jink:qingguo[%s:%s]=%d"):format(suit, number, card_id)

end

end

--[[与 ai_filterskill_filter 所不同的是,这里多出了一个参数 player ,这是指拥有该技能的玩家。

这个参数的作用可以见 standard-ai.lua 中 1109 至 1116 行关于急救的代码。]] sgs.ai_view_as.jijiu = function(card, player, card_place)

local suit = card:getSuitString()

local number = card:getNumberString()

local card_id = card:getEffectiveId()

if card:isRed() and player:getPhase()==sgs.Player_NotActive then -- 是红色牌,且在华佗的回合外。

return ("peach:jijiu[%s:%s]=%d"):format(suit, number, card_id)

end

end

--[[

通过前面三个 lua 文件的介绍,相信大家对 AI 的编写已经有了基本的认识。

这一文档将集中介绍与 smart-ai.lua 中第三部分“响应请求”的相关函数和类表。 从这一文档介绍的内容开始,需要为 AI 编写作决策的代码了。

为了让 AI 作出正确的决策,必须给 AI 以足够的信息,其中一个方法就是通过 data 传递数据。

这在 11-Fundamentals.lua 中已经有所提及,下面深入地介绍一下 data 相关的一些内容。

++ data 是什么?

data 是一个 QVariant 对象,这一特殊的类型使得它可以传递任意类型的信息。

可以是一个整数、一张牌,一个使用牌的结构体(CardUseStruct ),一个玩家(ServerPlayer*),等等。

++ 如何构造 data ?

在 C++ 里,通过 QVariant data = QVariant::fromValue(object) 创建 data 。

其中 object 可以是上面提到的任何一种对象。

在 Lua 里,通过下面的代码来构造 data 。]]

local data = sgs.QVariant() -- 构造一个空的 QVariant 对象

data:setValue(object) -- 为 QVariant 对象设置值,相当于上面的 QVariant::fromValue(object) --[[

++ 如何在 AI 里从 data 得到相应的值。

根据数据类型的不同,需要用不同的函数,列表如下。左边的是转换后得到的对象类型,右边是相应的转换函数。

这些内容实际上可以在 swig/qvariant.i 里面找到。注意所有的结构体都是以其指针的形式存在于 Lua 中的。

(更确切地说是一张特殊的表,更多细节可参见 SWIG 的文档)

number (数值类型) data:toInt() 注意:Lua 里面没有 int 类型,只有 number 类型

string (字符串) data:toString()

string 组成的表 data:toStringList() 注意:toStringList() 得到的是表(table )而不是列表(QList )

bool (布尔值) data:toBool()

DamageStruct* data:toDamage(), data:toDamageStar() 注意:两者没有实质上的区别 CardEffectStruct* data:toCardEffect()

SlashEffectStruct* data:toSlashEffect()

CardUseStruct* data:toCardUse()

CardMoveStruct* data:toCardMove()

Card* data:toCard()

ServerPlayer* data:toPlayer()

DyingStruct* data:toDying()

RecoverStruct* data:toRecover()

JudgeStruct* data:toJudge()

PindianStruct* data:toPindian()

关于上面提到的这些结构体的具体含义及其数据成员,已经超出本文档的范围。 请参见神大的讲座帖、roxiel 的教程和 src/server/structs.h

下面将给出 smart-ai.lua 中第三部分的一些函数及相关的表。

% skill_name:在下面的大多数函数中充当参数,表示技能名称

% self:下面大多数表的函数原型中的第一个参数,表示自身的 SmartAI 对象

下文中提到的大多数函数都与相应的表关联。所谓“默认行为”是指该表中相应的元素未定义的时候的行为。

! SmartAI:askForSuit()

该函数用于响应“选择花色”的请求。

% 返回值:Card::Suit 之一。即 sgs.Card_Spade, sgs.Card_Heart, sgs.Card_Club, sgs.Card_Diamond 之一

% 相关的表:暂无(以后会加入)

% 默认行为:按概率 2:2:2:3 的比例随机选择黑桃、红桃、草花、方片(此策略有待商榷) 目前与这一函数有关的技能只有周瑜的反间。

! SmartAI:askForSkillInvoke(skill_name, data):响应 Room::askForSkillInvoke 的函数。 该函数用于响应“是否发动技能 skill_name”的请求。

在用户界面中表现为“你要发动技能 XX 吗?”的提示框(Window )。

% 返回值:布尔值,表示是否发动该技能

% 相关的表:sgs.ai_skill_invoke

% 默认行为:如果技能的发动频率(Frequency )为 sgs.Skill_Frequent,则发动,否则不发动

* sgs.ai_skill_invoke:

% 元素名称:技能名

% 元素:布尔值或函数

%% 布尔值一般为 true ,表示该技能不管什么情况都发动

%% 函数,原型为:function(self, data)

%% 返回值:与 SmartAI.askForSkillInvoke (中的返回值,下略)含义相同

% 例子 1:mountain-ai.lua 第 168 行]]

sgs.ai_skill_invoke.tuntian = true -- 屯田总是发动

--[[% 例子 2:thicket-ai.lua 第 55 至 58 行,参见 11-Fundamentals.lua

! SmartAI:askForChoice(skill_name, choices):响应 Room::askForChoice 的函数

该函数用于响应 “请选择” 的请求。

在用户界面中表现为一个以技能名为标题,对应于每一个选择有一个按钮的对话框。 % choices: 表,包含所有可用选择

% 返回值:字符串,是 choices 中的一项,表示作出的选择

% 相关的表:sgs.ai_skill_choice

% 默认行为:通过函数 Skill::getDefaultChoice 获得技能的默认选择(默认选择缺省为 "no" ),

如果默认选择在 choices 中,则返回默认选择。否则随机返回 choices 中的一个元素。

* sgs.ai_skill_choice:

% 元素名称:技能名

% 元素:字符串值或函数

%% 字符串值:表明不论何种情况下都作出同一个给定的选择

%% 函数:原型为 function(self, choices)

%% choices, 返回值: 与 SmartAI.askForChoice 含义相同

% 例子 1:god-ai.lua 第 460 行]]

sgs.ai_skill_choice.jilve="zhiheng" -- 极略选择是制衡还是完杀时,总是选择制衡。 --% 例子 2:yitian-ai.lua 第 486 至 489 行关于义舍要牌的代码

sgs.ai_skill_choice.yisheask=function(self,choices)

assert(sgs.yisheasksource) -- 关于 assert ,见 15-Activate.lua

if self:isFriend(sgs.yisheasksource) then return "allow" else return "disallow" end -- 如果义舍要牌的请求来自友方,则接受请求,否则拒绝请求。

end

--[[

! SmartAI:askForDiscard(reason, discard_num, optional, include_equip):

响应 Room::askForDiscard 与 Room::askForExchange 的函数。

该函数用于响应 “请弃掉 X 张牌” 和 “请选择 X 张牌进行交换” 的请求。

在用户界面中表现为 “请弃掉 X 张牌” 和 “请选择 X 张牌进行交换” 的提示框。 % reason :字符串,弃牌的原因,一般为技能名。如果是正常的弃牌阶段的弃牌,则为“gamerule ”

% discard_num:数值,请求弃牌的张数

% optional:布尔值,是否可以选择不弃牌

% include_equip:布尔值,是否允许弃装备

% 返回值:表,包括所有要弃的牌的 ID

% 相关的表:sgs.ai_skill_discard

% 默认行为:若可以选择不弃牌,则不弃,否则按保留值从小到大依次弃牌。

如果允许弃装备且有 sgs.lose_equip_skill 中的技能,则优先弃装备。

* sgs.ai_skill_discard:

% 元素名称:弃牌原因(即 reason )

% 元素:函数,原型为 function(self, discard_num, optional, include_equip)

%% discard_num, optional, include_equip, 返回值:与 SmartAI.askForDiscard 含义相同 若返回 nil ,则执行默认行为。

% 例子;standard-ai.lua 第 82 至 108 行关于刚烈的代码。]]

sgs.ai_skill_discard.ganglie = function(self, discard_num, optional, include_equip)

if self.player:getHp() > self.player:getHandcardNum() then return {} end

-- 若体力值比手牌数多,则不弃牌。此策略有待商榷。

if self.player:getHandcardNum() == 3 then -- 手牌数为 3 时(临界情形)

local to_discard = {} -- 初始化 to_discard 为空表

-- 这一句不可省略,否则 table.insert(to_discard, ...) 会报错

local cards = self.player:getHandcards() -- 获得所有手牌

local index = 0

local all_peaches = 0

for _, card in sgs.qlist(cards) do

if card:inherits("Peach") then

all_peaches = all_peaches + 1 -- 计算出手牌中【桃】的总数。

end

end

if all_peaches >= 2 then return {} end -- 若至少有 2 张【桃】,则不弃牌。

for _, card in sgs.qlist(cards) do

if not card:inherits("Peach") then

table.insert(to_discard, card:getEffectiveId())

-- 把不是【桃】的牌的 ID 加入到弃牌列表之中

index = index + 1

if index == 2 then break end -- 若弃牌列表中已经有两张牌的 ID ,则中止循环

-- 此处去除局部变量 index 而改用 #to_discard 会使代码更为简洁

end

end

return to_discard -- 返回弃牌列表

end

if self.player:getHandcardNum() < 2 then return {} end -- 若手牌数不足 2 张,则无法弃牌。

end -- 其它情况,按照默认行为(弃牌阶段的策略)弃牌。

--[[

在这里插一句,从上面的注释可以看到现在 AI 的策略还有很多不完善的地方。 代码也还比较脏,这正是以后需要逐步努力改进的。

! SmartAI:askForNullification(trick_name, from, to, positive):

响应 Room::askForNullification 的函数,该函数用于响应“是否使用【无懈可击】”的请求。 % trick_name:Card* 类型,表示对何张锦囊牌使用无懈可击

(本变量名易使人误以为是字符串类型,将在以后修改)

% from:ServerPlayer*,trick_name 的使用者(不是【无懈可击】的使用者)

% to: ServerPlayer*,trick_name 的使用对象

% positive:为 true 时,本【无懈可击】使 trick_name 失效,否则本【无懈可击】使 trick_name 生效

% 返回值:Card*,决定使用的【无懈可击】。如果为 nil ,表示不使用【无懈可击】 % 相关的表:无

% 默认行为:较复杂,简单地说就是根据锦囊是否对己方有利决定是否使用。

有兴趣的可参见 smart-ai.lua 中的源代码。

! SmartAI:askForCardChosen(who, flags, reason):响应 Room::askForCardChosen 的函数 该函数用于响应“请从给定的牌中选择一张”的请求。

在用户界面中表现为类似使用【顺手牵羊】的时候出现的对话框。

% who:ServerPlayer*,从何人的牌中选择

% flags:字符串,"h", "e", "j" 的任意组合,参见 12-SmartAI.lua

% reason:字符串,请求的原因,可能是技能名或者是卡牌的对象名(后者如 "snatch" ) % 返回值:数值,选择的实体卡的 ID 。

% 相关的表:sgs.ai_skill_cardchosen

% 默认行为:即使用【过河拆桥】的时候选择牌的策略,较为复杂,有兴趣的可参见 smart-ai.lua 中的源代码。

? sgs.ai_skill_cardchosen:

% 元素名称:reason ,其中所有的短横 "-" 要用下划线 "_" 取代。

% 元素:函数,原型为 cardchosen(self, who)

%% who:与 SmartAI.askForCardChosen 含义相同

%% 返回值:选择的卡牌

返回值为 nil 时,执行默认行为。

% 例子:只有一个,在 mountain-ai.lua 第 70 至 74 行。这是因为绝大部分情况下默认行为已经能满足要求。]]

sgs.ai_skill_cardchosen.qiaobian = function(self, who, flags)

if flags == "ej" then

return card_for_qiaobian(self, who, "card")

-- 调用 mountain-ai.lua 第 1 行开始定义的辅助函数得到结果。

end

end

--[[

! SmartAI:askForCard(pattern, prompt, data):响应 Room::askForCard 的函数

该函数用于响应“请打出一张牌”的请求。

在用户界面中表现为一个提示框,框内文字由 prompt 与翻译文件决定。

% pattern:字符串,用于匹配的模式串

% prompt:字符串,表示提示信息

为后面描述的方便起见,这里需要介绍一下 prompt 的标准格式。

它与翻译文件里面以百分号开头的参数占位符一一对应。

最一般的 prompt 格式如下:

"%prompt_type:%src:%dest:%arg:%2arg"

%% %prompt_type:表示 prompt 的类型,决定了将会读取翻译文件中的哪一个条目。 %% %src:为一个 ServerPlayer 对象的对象名("sgsX" ),翻译文件中的 %src 将以相应的武将名代入

%% %dest:为一个 ServerPlayer 对象的对象名("sgsX" ),翻译文件中的 %dest 将以相应的武将名代入

%% %arg, %2arg:为自定义的额外参数,将直接翻译后代入。

更详细的说明属于技能编写范畴不属于 AI 范畴,故不再赘述。

% 返回值:字符串,将用于 Card::Parse 得到实际的卡牌,如果为 "." ,表示不打出任何卡。 % 相关的表:sgs.ai_skill_cardask

% 默认行为:当 pattern 为 "slash" 或 "jink" 的时候,

在符合 pattern 的卡牌中随机挑选一张返回其 ID ,如果没有任何卡牌满足要求则返回 "." 其余情况一律返回 "." (在目前版本的 AI 中,返回值可能为 nil ,留待以后修正)。

* sgs.ai_skill_cardask:

% 元素名称:就是 %prompt_type

% 元素:函数,原型为 function(self, data, pattern, target, target2)

这是最完整的函数原型,实际上如果不需要用到后面的参数可以直接省略。

%% pattern,返回值:与 SmartAI.askForCard 含义相同

%% target:ServerPlayer*,表示与 %src 相应的 ServerPlayer 对象

%% target2:ServerPlayer*,表示与 %dest 相应的 ServerPlayer 对象

% 例子:standard-ai.lua 第 55 至 67 行关于鬼才的代码。]]

sgs.ai_skill_cardask["@guicai-card"]=function(self) -- 仅仅用到了第一个参数 self ,后面的都可以省略

local judge = self.player:getTag("Judge"):toJudge() -- 获得判定结构体

if self:needRetrial(judge) then -- 若需要改判

local cards = sgs.QList2Table(self.player:getHandcards()) -- 获得手牌的表

local card_id = self:getRetrialCardId(cards, judge) -- 从所有手牌中寻找可供改判的牌 if card_id ~= -1 then

return "@GuicaiCard=" .. card_id -- 若找到则改判

end

end

return "." -- 若不需要改判或没有可供改判的牌,则不打出任何牌

end

--[[

注意:这里的 AI 与旧版的不相同。

在旧版 AI 中,对应的第一行为 sgs.ai_skill_invoke["@guicai"] = function(self, prompt)。 已经仿照旧版 AI 编写改判技能 AI 的,请按照新的要求对相应的代码进行修改。

! SmartAI:askForUseCard(pattern, prompt):响应 Room::askForUseCard 的函数

该函数用于响应“请使用一张牌”的请求。

在用户界面中表现为一个提示框,框内文字由 prompt 与翻译文件决定。

% pattern, prompt:与 SmartAI.askForCard 含义相同

% 返回值:字符串,将用于 CardUseStruct::parse 得到实际的卡牌使用结构体(CardUseStruct ) 如果为 "." ,表示不使用任何卡牌。

% 相关的表:sgs.ai_skill_use

% 默认行为:返回 "." 。

此处必须引入对辅助函数的介绍:

void CardUseStruct::parse(const QString &str, Room *room):根据字符串 str 设定卡牌使用结构体。

str 的格式如下:"%card_str->%target_str",注意其中没有多余的空格。

%card_str 是一个可以被 Card::Parse 解析的字符串,表示所使用的卡牌(可以是实体卡或虚拟卡)

此部分字符串决定了成员 card 的内容。

%target_str 是一个表示卡牌使用对象的字符串,它由一个或多个 ServerPlayer 对象的对象名("sgsX" )用加号连接而成。

%target_str 也可以是 "." ,表示卡牌没有使用对象(但是这种情况一般在编写技能时用 askForCard 就可以满足要求)。

此部分字符串决定了成员 to 的内容。

尽管有着与 sgs.Card_Parse 类似的函数 sgs.CardUseStruct_parse (注意 p 的大小写), 但是迄今为止在 AI 代码中还没有直接调用过这一函数,也不建议大家在编写 AI 时使用。

对于 AI 的编写来说,CardUseStruct::from 永远自动设定为 self.player ,而成员 card 和 to 则需要由返回值字符串指定。

* sgs.ai_skill_use:

% 元素名称:pattern (注意与 sgs.ai_skill_cardask 的元素名称不同)

% 元素:函数,原型为 function(self, prompt)

%% prompt, 返回值:与 SmartAI.askForUseCard 含义相同

其中返回值可以为 nil ,此时 SmartAI.askForUseCard 返回 "." 。

% 例子:yitian-ai.lua 中第 243 至 253 行关于连理的代码。]]

sgs.ai_skill_use["@lianli"] = function(self, prompt)

-- 元素名(即 pattern )为 "@lianli",含有特殊字符 "@",用方括号而不是点作索引 -- 关于方括号和点的区别和联系,见 11-Fundamentals.lua

self:sort(self.friends) -- 将自己的所有友方按嘲讽值排序,关于 SmartAI.sort ,见 12-SmartAI.lua

for _, friend in ipairs(self.friends) do

if friend:getGeneral():isMale() then -- 找到嘲讽最高的友方男性角色

return "@LianliCard=.->" .. friend:objectName() -- 对他使用连理技能卡 end

end

return "." -- 找不到的时候,不使用连理技能卡

end

--[[

! SmartAI:askForAG(card_ids, refusable, reason):响应 Room::askForAG 的函数

该函数用于响应“请从展示的数张牌中选择一张”的请求,例如五谷、心战的第一阶段等。 在用户界面中表现为类似【五谷丰登】使用时的框。(CardContainer )

% card_ids:表,包含所有可供选择的卡牌的 ID

% refusable:布尔值,表示是否可以不选

% reason:字符串,表示请求选择的原因,对于因技能发起的请求一般为技能名。 % 返回值:数值,选择的牌的 ID 。如果为 -1,表示随机选择一张。

% 相关的表:sgs.ai_skill_askforag

% 默认行为:即【五谷丰登】的选牌策略,调用 SmartAI.sortByCardNeed 对卡牌需要程度进行排序,选择最需要的卡牌。

* sgs.ai_skill_askforag:

% 元素名称:reason ,其中所有的短横 "-" 要用下划线 "_" 取代。

% 元素:函数,原型为 function(self, card_ids)。

%% card_ids, 返回值:与 SmartAI.askForAG 含义相同。

% 例子:wind-ai.lua 第 258 行关于不屈吃桃的时候选择要去掉的不屈牌的代码。]] sgs.ai_skill_askforag.buqu = function(self, card_ids)

for i, card_id in ipairs(card_ids) do

for j, card_id2 in ipairs(card_ids) do

if i ~= j and sgs.Sanguosha:getCard(card_id):getNumber() == sgs.Sanguosha:getCard(card_id2):getNumber() then

-- 若两张牌 ID 不等,但是点数相同

return card_id -- 返回找到的第一组相同点数的卡的第一张的 ID end

end

end

return card_ids[1] -- 返回第一张(相当于随机)

end

--[[

! SmartAI:askForCardShow(requestor, reason):响应 Room::askForCardShow 的函数 该函数用于响应“展示一张卡牌”的请求。

在用户界面中表现为“XX 要求你展示一张手牌”的提示框。

% requestor:ServerPlayer*,表示请求的发出者

% reason:字符串,请求发出的原因,对于因技能发起的请求一般为技能名

% 返回值:Card*,表示展示的卡牌

% 相关的表:sgs.ai_cardshow

% 默认行为:返回随机一张手牌

* sgs.ai_cardshow:

% 元素名称:reason

% 元素:函数,原型为 function(self, requestor)

%% requestor, 返回值:与 SmartAI.askForCardShow 相同

% 例子:maneuverint-ai.lua 第 253 至 276 行关于对方使用火攻时展示牌的策略。]] sgs.ai_cardshow.fire_attack = function(self, requestor)

local priority =

{

heart = 4,

spade = 3,

club = 2,

diamond = 1

} -- 优先级,红桃最优先展示,方片最次。

local index = 0

local result

local cards = self.player:getHandcards()

for _, card in sgs.qlist(cards) do

if priority[card:getSuitString()] > index then -- 若找到了优先级更高的卡牌

result = card -- 则应展示优先级更高的卡牌

index = priority[card:getSuitString()]

end end

if self.player:hasSkill("hongyan") and result:getSuit() == sgs.Card_Spade then -- 有技能 “红颜” 且卡牌为黑桃时

result = sgs.Sanguosha:cloneCard(result:objectName(), sgs.Card_Heart, result:getNumber()) -- 转换为红桃,点数和种类不变

result:setSkillName("hongyan") -- 展示的卡牌的技能名为 “红颜”

end

return result -- 返回结果

end

--[[

! SmartAI:askForYiji(cards):用于响应 Room::askForYiji 的函数

该函数仅与“遗计”技能相关,故说明略去。

! SmartAI:askForPindian(requestor, reason):用于响应 Room::askForPindian 的函数 该函数用于响应 “打出一张牌进行拼点” 的请求。

在用户界面表现为 “打出一张牌进行拼点” 的提示框。

% requestor:ServerPlayer*,请求的发出者

% reason:字符串,请求发出的原因,对于因技能发起的请求一般为技能名

% 返回值:Card*,用于拼点的卡牌

% 相关的表:暂无(以后会加入)

% 默认行为:返回点数最大的手牌

! SmartAI:askForPlayerChosen(targets, reason):用于响应 Room::askForPlayerChosen 的函数 该函数用于响应 “在给定的范围内选择一名玩家” 的请求。

在用户界面表现为 “请选择一名玩家” 的提示框。

% targets:列表(QList ),包含所有可供选择的玩家

% reason:字符串,请求发出的原因,对于因技能发起的请求一般为技能名

% 返回值:ServerPlayer*,选择的玩家

% 相关的表:sgs.ai_skill_playerchosen

% 默认行为:在 targets 中随机选择一名角色返回。

* sgs.ai_skill_playerchosen:

% 元素名称:reason ,其中所有的短横 "-" 要用下划线 "_" 取代。

% 元素:函数,原型为 function(self,targets)

%% targets, 返回值:含义与 SmartAI.askForPlayerChosen 相同

返回值可以为 nil ,此时执行默认行为。

% 例子:mountain-ai.lua 第 254 至 260 行关于放权的代码。]]

sgs.ai_skill_playerchosen.fangquan = function(self, targets)

for _, target in sgs.qlist(targets) do

if self:isFriend(target) then

return target -- 返回第一个友方

end

end

end

--[[

为了提高代码的重用性,方便 AI 的编写,在 smart-ai.lua 和 standard-ai.lua 中定义了两个标准的策略。

* sgs.ai_skill_playerchosen.damage:选择一名角色对其造成 1 点伤害

* sgs.ai_skill_playerchosen.zero_card_as_slash:选择一名角色,视为对其使用了一张无花色无属性的【杀】。

下面的代码说明了这些 “标准策略” 的使用:]]

sgs.ai_skill_playerchosen.luanwu = sgs.ai_skill_playerchosen.zero_card_as_slash -- 乱武 sgs.ai_skill_playerchosen.quhu = sgs.ai_skill_playerchosen.damage -- 驱虎

-- 技能“旋风”是一个很好的例子:

sgs.ai_skill_playerchosen.xuanfeng_damage = sgs.ai_skill_playerchosen.damage

sgs.ai_skill_playerchosen.xuanfeng_slash = sgs.ai_skill_playerchosen.zero_card_as_slash --[[

! SmartAI:askForSinglePeach(dying):用于响应 Room::askForSinglePeach 的函数

该函数用于响应濒死求桃的请求。在用户界面上表现为 “XX 正在死亡线上挣扎” 的提示框。

% dying: ServerPlayer*,处于濒死状态的角色

% 返回值:字符串,经 Card::Parse 之后得到实际的卡牌。]]

--[[

在前面五个文档中,您已经学习了在开启身份预知的情况下,让 AI 按照您的要求去工作的所有基本知识了。

为了真正熟悉 AI 的编写,接下来您需要做的只是不断地模仿和练习。

下面你可以根据自己的情况作出选择:

+ 如果您对身份判断不感兴趣,或者认为您添加的技能和卡牌对身份判断影响很小。 您可以直接跳到 17-Example.lua ,并且仅仅阅读其中与身份判断无关的部分。

+ 如果您希望进一步了解身份判断部分的 AI ,欢迎继续阅读本文档。

本文档将集中介绍 smart-ai.lua 中第二部分“身份、动机、仇恨值”的内容。

首先需要树立一个概念,在一局游戏中,所有的 AI 共享同一套身份判断有关的数据。 而并不是像一些人想象的那样,每个 AI 有自己的身份判断数据。

另外,下面介绍的内容均以 8 人身份局为例。

对于某些很特殊的情况(例如国战),此处不作介绍。

++ 与 AI 身份判断有关的表

(注意,它们都是 sgs 而不是 SmartAI 的元素,这就印证了上面的说法)

下面这些表的元素名称,如果没有特别说明,都是 ServerPlayer 对象的对象名,即形如 "sgsX" 的字符串。

* sgs.ai_loyalty:表,包含忠诚度

% 元素:数值,为元素名称所对应的玩家的忠诚度,取值范围为 [-160, 160]。

数值越大越忠诚,初始值为 0,主公的忠诚度永远为 160。

? sgs.ai_anti_lord:表

% 元素:数值,为元素名称对应的玩家的明确攻击主公的次数,取值范围为非负数。 初始值为 nil

! sgs.ai_renegade_suspect:表,包含内奸的可疑程度

% 元素:数值,为元素名称对应的玩家的内奸可疑程度,取值范围为非负数。

数值越大越像内奸,初始值为 nil

? sgs.ai_explicit:表,包含玩家目前表现出的身份

% 元素:字符串,为以下四个值之一:

%% loyalist:忠臣(sgs.ai_loyalty 达到 160)

%% loyalish:跳忠,但没有到可以判为忠臣的程度(sgs.ai_loyalty 达到 80,但没达到 160) %% rebel:反贼(sgs.ai_loyalty 达到 -160)

%% rebelish:跳反,但没有到可以判为反贼的程度(sgs.ai_loyalty 小于 -80,但没达到 -160) 初始值为 nil ,主公的取值永远为 "loyalist" 。

在目前版本的 AI 中,对忠臣与跳忠,反贼与跳反之间的区别并不明晰。

(表现为很多时候对待忠臣与对待跳忠玩家的策略是完全相同的,反贼也是)

这有待以后逐步完善。

还有一些其它的表,因为还不完善,在此不作介绍。

++ AI 如何运用与身份判断有关的表里面的数据

将上面几个表整合起来得到敌友关系的是如下的重要函数:

* SmartAI:objectiveLevel(player):获得对玩家 player 的动机水平。

% player:ServerPlayer*

% 返回值:数值,表示自身对 player 的动机水平。

动机水平为负,表示 player 是自己的友方。

动机水平介乎 0 到 3 之间(含 0 与 3),表示 player 是自己的敌方,但是不会对 player 使用【杀】。

动机水平大于 3,表示 player 是自己的敌方,且会对 player 使用【杀】。

总的来说,动机水平越负,表示友善程度越高。

目前的 AI 中,动机水平取值范围为 [-3, 6]。

此函数是 AI 中被调用最为频繁的函数之一。

想进一步了解 SmartAI.objectiveLevel 是怎么具体运用上面几个表里面的数据而最终得到动

机水平的,

可以参见 smart-ai.lua 中的相关源码,此处不作介绍。

在 12-SmartAI.lua 中介绍的一系列与敌友关系有关的函数,都是基于 SmartAI.objectiveLevel 的。

当然,如果开启了身份预知,那么这些与敌友关系有关的函数将直接调用源代码中 AI 部分的相应代码。

在此顺带介绍一下与身份预知关系最密切的一个函数。

! sgs.isRolePredictable():是否按照身份预知下的策略去运行 AI

% 返回值:布尔值,含义同上。

注意 “是否按照身份预知下的策略去运行 AI ” 与 “是否在服务器窗口中勾选了身份预知” 是两个不同的概念。

更多细节请参看 sgs.isRolePredictable() 的源代码。

此外,还需要知道下面这一重要函数:

* SmartAI:updatePlayers(inclusive):更新与敌友关系有关的列表。

% inclusive:布尔值,此处不作介绍。

% 返回值:nil 。

% 相关表格:sgs.ai_global_flags

此函数有以下几个作用:

+ 将 sgs.ai_global_flags 指明的所有元素重置为 nil (详见下面的说明)

+ 生成表 self.friends, self.friends_noself, self.enemies。

此函数也是 AI 中被调用最为频繁的函数之一。

如果你有任何技能的 AI 代码觉得在开始之前更新一下这几个列表会比较好,可以直接在技能代码中调用]]

self:updatePlayers()

--[[

* sgs.ai_global_flags:表,包括表 sgs 中所有需要重置为 nil 的元素名称。

% 元素名称:无,用 table.insert 把元素加入本表

% 元素:字符串,要重置的元素名称。

用下面一个例子来说明,设原来]]

sgs.ai_global_flags == {"abc", "def-ghi", "@foo"}

sgs.abc == 3

sgs["def-ghi"] == true

sgs["@foo"] == function(a, b)

return a + b/2 + 3

end

-- 则在执行 SmartAI.updatePlayers 之后

sgs.ai_global_flags == {"abc", "def-ghi", "@foo"}

sgs.abc == nil

sgs["def-ghi"] == nil

sgs["@foo"] == nil

end

end

--[[

介绍完这些重要的工具函数之后,可以开始讲述 AI 如何从具体的游戏过程中设置与身份判断有关的表的数据了。

这一部分内容略为难以理解,但却是未来 AI 身份判断进一步完善的突破口。

如果没有兴趣了解更多细节,可以直接跳到本文档后面关于 sgs.ai_card_intention 的描述。 不阅读这一部分对于实际 AI 的编写影响很小。

---------------------选读部分开始---------------------

在这一部分,AI 的角色变了,AI 不再是决策者,而成为了游戏的观察者和记录者。

这部分 AI 的主要任务,就是处理游戏中发生的各种事件,得出这些事件背后意味着的敌友关系。

而很容易理解,对于一局游戏来说,只要有一个记录者就够了。

! sgs.recorder:一个特殊的 SmartAI 对象,它是游戏的记录者

! SmartAI:filterEvent(event, player, data):记录发生的事件。

% event :TriggerEvent 类型,表示事件的种类,详情请参见神主的讲座帖和 src/server/structs.h。

% player:ServerPlayer*,事件发生的对象

% data:QVariant*,与事件有关的数据

% 返回值:nil

% 相关的表:很多,详见下述。

这一函数当 self 不是 sgs.recorder 的时候将不会执行任何与身份判断有关的处理。 当 self 是 sgs.recorder 的时候,会根据事件不同作出不同的处理。

这一函数会在以下事件发生时调用 SmartAI.updatePlayer :

sgs.CardUsed, sgs.CardEffect, sgs.Death, sgs.PhaseChange, sgs.GameStart

接下来要介绍一个在编写扩展的时候从来不需要用到,但是对 AI 却很重要的事件:sgs.ChoiceMade 。

这个事件表示 player 作出了某种选择。

这个事件之所以如此重要,是因为它具有超前性,例如,“作出使用某张牌的选择” 明显比 sgs.CardUsed 时间要超前得多。

后者发生在卡牌使用的结算完成之后。

这个事件在解决循环激将的问题上起到了关键作用。下面会具体说明。

接下来介绍与 sgs.ChoiceMade 有关的表:

* sgs.ai_choicemade_filter:表,包含与 sgs.ChoiceMade 有关的身份判断的 AI 代码。 % 元素名称与元素:目前版本的 AI 中共 5 个。

%% cardUsed:表,包含“决定使用某张卡牌”相关的事件响应函数

%%% 元素名称:无,用 table.insert 把元素加入本表

%%% 元素:函数,原型为 function(player, carduse)

%%%% player,返回值:与 SmartAI.filterEvent 含义相同

%%%% carduse:CardUseStruct*,卡牌使用结构体,用于描述作出的决策。

不难理解,大部分与 AI 出牌有关的身份判断代码应该放在这一部分。

但是,目前由于历史原因,大部分使用卡牌的身份判断还是交给了 sgs.CardUsed 事件去处理,

即在卡牌结算结束后处理。

%% cardResponsed:表,包含“决定打出某张卡牌或不打出任何卡牌”相关的事件响应函数 %%% 元素名称:Room::askForCard 当中的 prompt

%%% 元素:函数,原型为 function(player, promptlist)

%%%% player,返回值:与 SmartAI.filterEvent 含义相同

%%%% promptlist:表

%%%%% promptlist[2]:Room::askForCard 或者 Room::askForUseCard 当中的 pattern

%%%%% promptlist[#promptlist]:字符串,若为 "_nil_",表示决定不打出卡牌,否则表示打出了卡牌。

%% skillInvoke:表,包含“决定发动或不发动某项技能”相关的事件响应函数

%%% 元素名称:技能名

%%% 元素,原型为 function(player, promptlist)

%%%% player,返回值:与 SmartAI.filterEvent 含义相同

%%%% promptlist:表

%%%% promptlist[3]:字符串,若为 "yes" ,表示选择发动此技能,否则选择不发动。

%% skillChoice:表,包含“决定选择某一项”相关的事件响应函数

%%% 元素名称:技能名

%%% 元素,原型为 function(player, promptlist)

%%%% player,返回值:与 SmartAI.filterEvent 含义相同

%%%% promptlist:表

%%%% promptlist[3]:字符串,为所作的选择。

%% Nullification:表,包括“决定使用无懈可击”相关的事件响应函数

这个表目前很少用到,不作介绍。

% 例子:standard-ai.lua 第 472 至 497,531 至 536 行关于激将的代码。]]

-- sgs.jijiangsource 用于记录激将的来源

table.insert(sgs.ai_global_flags, "jijiangsource")

-- 每当执行 SmartAI.updatePlayers 时,清除激将的来源

local jijiang_filter = function(player, carduse)

if carduse.card:inherits("JijiangCard") then -- 如果有人使用了激将技能卡

sgs.jijiangsource = player -- 记录激将来源

else -- 如果有人使用了其它卡(注意是使用不是打出)

sgs.jijiangsource = nil -- 清除激将来源

end

end

table.insert(sgs.ai_choicemade_filter.cardUsed, jijiang_filter)

-- 把上面的函数注册到 sgs.ai_choicemade_filter 里面

sgs.ai_skill_invoke.jijiang = function(self, data)

local cards = self.player:getHandcards()

for _, card in sgs.qlist(cards) do

if card:inherits("Slash") then

return false

end

end

if sgs.jijiangsource then return false else return true end

-- 如果已经有人在激将,则不发动激将

end

sgs.ai_choicemade_filter.skillInvoke.jijiang = function(player, promptlist)

if promptlist[#promptlist] == "yes" then-- 如果有人在要求打出【杀】询问是否发动激将时,选择了发动激将

sgs.jijiangsource = player -- 记录下激将的来源

end

end

-- 中间数行代码略去

sgs.ai_choicemade_filter.cardResponsed["@jijiang-slash"] = function(player, promptlist) if promptlist[#promptlist] ~= "_nil_" then -- 如果有人响应了激将

sgs.updateIntention(player, sgs.jijiangsource, -40) -- 响应激将者对激将来源的仇恨值为 -40

sgs.jijiangsource = nil -- 当有人响应时,激将的结算结束,故清除激将来源 end

end

--[[

这个例子完整地展示了 sgs.ai_choicemade_filter 的应用。

如果您能够读懂这部分的代码,并且理解这部分的代码是如何防止循环激将的。您对 AI 的了解已经很不错了。

当然,读不懂也没关系,因为像激将这样的技能毕竟还是少数。

SmartAI.filterEvent 除了会处理 sgs.ChoiceMade ,以及如前所述在一系列事件发生时调用 SmartAI.updatePlayers 之外,

还会处理下面的事件:sgs.CardEffect, sgs.Damaged, sgs.CardUsed, sgs.CardLost, sgs.StartJudge。 很遗憾,在目前版本的 AI 中,大部分的事件都没有提供可供扩展使用的接口。唯一提供了接口的是 sgs.CardUsed 。

与这个事件相应的表是下面这个:

---------------------选读部分结束---------------------

* sgs.ai_card_intention:表,包括与卡牌使用相关的仇恨值

% 元素名称:卡牌的类名

% 元素:数值或函数

%% 函数:原型为 (card, from, tos, source)

%%% card:Card*,所使用的卡牌

%%% from:ServerPlayer*,卡牌的使用者

%%% tos:表,包括卡牌的所有使用对象

%%% source:ServerPlayer*,在卡牌使用时处于出牌阶段的玩家

在这个函数中,需要手动调用 sgs.updateIntention 来指明仇恨值。

%% 数值:表明卡牌使用者对卡牌使用对象的仇恨值。

实际上会自动调用函数 sgs.updateIntentions

对于绝大部分的卡牌,用数值已经可以满足要求。

% 例子 1:standard-ai.lua 第 1105 行关于青囊的代码。]]

sgs.ai_card_intention.QingnangCard = -100 -- 青囊的使用者对使用对象的仇恨值为 -100

-- 例子 2:maneuvering-ai.lua 第 180 至 188 行关于铁索连环的代码。

sgs.ai_card_intention.IronChain=function(card,from,tos,source)

for _, to in ipairs(tos) do -- tos 是一个表

if to:isChained() then -- 若使用对象处于连环状态

-- 注意这里指的是铁索连环使用完之后的状态

sgs.updateIntention(from, to, 80) -- 使用者对使用对象的仇恨值为 80 else

sgs.updateIntention(from, to, -80)

end

end

end

-- 下面以倚天包中义舍的技能代码为例,说明如何综合运用前面介绍的内容来编写完整的技能 AI

-- 这段代码位于 yitian-ai.lua 第 449 至 514 行

local yishe_skill={name="yishe"}

table.insert(sgs.ai_skills,yishe_skill) -- 注册义舍技能

yishe_skill.getTurnUseCard = function(self) -- getTurnUseCard 见 15-Activate.lua

return sgs.Card_Parse("@YisheCard=.") -- 义舍技能卡,未指定子卡

end

sgs.ai_skill_use_func.YisheCard=function(card,use,self) -- 义舍技能卡的使用函数

-- ai_skill_use_func 见 15-Activate.lua

-- 这同时包括两种情况:把手牌加入到“米”中与把“米”中所有牌收回到手牌,故

需分情况讨论

if self.player:getPile("rice"):isEmpty() then -- 第一种情况

local cards=self.player:getHandcards()

cards=sgs.QList2Table(cards) -- sgs.QList2Table 见 11-Fundamentals.lua

local usecards={}

for _,card in ipairs(cards) do -- 先加入所有的【屎】牌

if card:inherits("Shit") then table.insert(usecards,card:getId()) end

end

local discards = self:askForDiscard("gamerule", math.min(self:getOverflow(),5-#usecards))

-- 按照弃牌阶段弃牌的策略来选择追加到“米”里面的牌

-- 5-#usecards 表示最多只能义舍 5 张牌

-- 需追加到义舍牌中的牌应该是手牌的溢出部分,且保证追加后“米”数不超过 5

-- SmartAI.getOverflow 见 12-SmartAI.lua

-- SmartAI.askForDiscard 见 14-Responsing.lua

for _,card in ipairs(discards) do -- 把追加的牌插入到“米”中

table.insert(usecards,card)

end

if #usecards>0 then -- 若有牌可以义舍

use.card=sgs.Card_Parse("@YisheCard=" .. table.concat(usecards,"+"))

-- sgs.Card_Parse 见 13-ViewAs.lua

end

else -- 第二种情况 if not self.player:hasUsed("YisheCard") then use.card=card return end

-- 仅当回合内没有使用过义舍,即“米”不是刚刚放进去时,才发动义舍把“米”回收到手牌

-- ServerPlayer::hasUsed 的用法见 15-Activate.lua

end

end

table.insert(sgs.ai_global_flags, "yisheasksource") -- sgs.ai_global_flags 见 16-RoleJudgement.lua

local yisheask_filter = function(player, carduse)

if carduse.card:inherits("YisheAskCard") then

sgs.yisheasksource = player -- 记录下义舍要牌的请求来源

else

sgs.yisheasksource = nil

end

end

table.insert(sgs.ai_choicemade_filter.cardUsed, yisheask_filter) -- 注册到 sgs.ai_choicemade_filter 中

-- sgs.ai_choicemade_filter 见 16-RoleJudgement.lua

sgs.ai_skill_choice.yisheask=function(self,choices) -- sgs.ai_skill_choice 见 14-Responsing.lua -- 决定是否接纳义舍要牌的请求

assert(sgs.yisheasksource) -- 验证义舍要牌的来源已经正确设置,避免出错

-- assert 见 15-Activate.lua

if self:isFriend(sgs.yisheasksource) then return "allow" else return "disallow" end -- 如果是友方,则同意请求,否则拒绝请求

end

-- 对于已经详细阅读过 16-RoleJudgement.lua 的朋友,不妨思考一下

-- 如果想告诉 AI ,接纳义舍要牌的请求属于友善的行为,该写什么代码

local yisheask_skill={name="yisheask"}

table.insert(sgs.ai_skills,yisheask_skill) -- 注册义舍要牌技能

yisheask_skill.getTurnUseCard = function(self)

for _, player in sgs.qlist(self.room:getOtherPlayers(self.player)) do

if player:hasSkill("yishe") and not player:getPile("rice"):isEmpty() then return sgs.Card_Parse("@YisheAskCard=.") end

-- 如果有人有义舍技能且其“米”牌堆不为空,则考虑使用义舍要牌技能卡的可能性

-- 此处可以用 self.room:findPlayerBySkillName 简化代码

end

end

sgs.ai_skill_use_func.YisheAskCard=function(card,use,self) -- 义舍要牌技能卡的使用函数 if self.player:usedTimes("YisheAskCard")>1 then return end -- 每回合最多义舍要牌两次 -- 这里说明了对于限制回合内的最大使用次数的牌,其 AI 该怎样书写

local zhanglu

local cards

for _, player in sgs.qlist(self.room:getOtherPlayers(self.player)) do

if player:hasSkill("yishe") and not player:getPile("rice"):isEmpty() then zhanglu=player cards=player:getPile("rice") break end

end

-- 同样,可以用 self.room:findPlayerBySkillName 简化代码

if not zhanglu or not self:isFriend(zhanglu) then return end -- 如果不是友方,则不发出请求

cards = sgs.QList2Table(cards)

for _, pcard in ipairs(cards) do

if not sgs.Sanguosha:getCard(pcard):inherits("Shit") then -- 若“米”牌中至少有一张不是【屎】牌,则发出请求

use.card = card

return -- 已经正确设置 use.card ,直接返回

end

end

end

--[[ 本文档用于介绍在 beta 版中引入的更新

本文档针对的 beta 版本为 20120203(V0.7 Beta 1)

本文档假定阅读者已经有一定的编写 AI 的基础,仅作十分简略的介绍。

新的函数与表使得 AI 的可扩展性大大增强。

* sgs.ai_slash_prohibit:表,由 SmartAI.slashProhibit 载入

% 元素名称:技能名

% 元素:function(self, to, card)

%% self: SmartAI

%% to: ServerPlayer*,拥有元素名称所描述的技能

%% card: Card*

%% 返回值:布尔值,true 表明在策略上不宜对 to 使用【杀】 card 。

本表设置后,以后编写技能将基本不需修改 SmartAI.slashProhibit

* sgs.ai_cardneed:表,由 SmartAI.getCardNeedPlayer (新函数,见下面的描述) 载入 % 元素名称:技能名

% 元素;function(friend, card, self)

%% friend:ServerPlayer*,拥有元素名称所描述的技能

%% card: Card*

%% self: SmartAI

%% 返回值:布尔值,true 表明友方玩家 friend 需要卡牌 card

标准元素:sgs.ai_cardneed.equip 和 sgs.ai_cardneed.bignumber,详见 smart-ai.lua

* SmartAI:getCardNeedPlayer(cards);在 cards 中找到一张用于仁德/遗计/任意类似技能的卡牌及使用对象

% cards:表,包含可用的卡牌

% 返回值:一个二元组 Card*, ServerPlayer*,表示卡牌及其使用对象。

引入这一个表和一个函数之后,以后想要刘备认识您编写的新技能而按照您的需要仁德卡牌给您,就变得十分简单了。

* sgs.ai_skill_pindian:表,用于 SmartAI.askForPindian

% 元素名称:reason

% 元素:function(minusecard, self, requstor, maxcard, mincard)

%% minusecard:自己手牌中使用价值最小的卡牌

%% self:SmartAI

%% requestor:ServerPlayer*,请求来源

%% maxcard:手牌中使用价值低于 6 的卡牌中点数最大者

%% mincard:手牌中使用价值低于 6 的卡牌中点数最小者

%% 返回值:Card*,一般可以直接用参数中的一个返回。

若为 nil ,表示采用默认策略。

默认策略即:若 requestor 是友方,返回 mincard ,否则返回 maxcard 。