8. 服务端:人机出牌逻辑#
备注
人机仍在施工中,本文内容仅用来说明目前的思路,随时可能过时。
人机出牌逻辑(AI)是一套应对请求与答复的程序,当需要做出答复的玩家并没有真人玩家能前来响应时,程序就要自己代替玩家计算出一个较为合理的方案并返回给Request。
新月杀的目标之一是运行在低端设备上(手机、VPS等),为此人机出牌逻辑无法使用当前流行的人工智能方案,算力不够。为此如同大多数民间版三国杀一样,新月杀在AI上的选择依然是手动为各种询问编写作答策略。
8.1. AI的整体思路#
待处理
画图。
AI体系整体上分为三层,第一层是直接和Request类打交道的,给Request类暴露一个方法,
调用该方法即可获得计算出的答复。这个方法就是 makeReply 。makeReply的根据主要有两个:command和data,也就是Request传给每个玩家的询问类型与询问数据。除此之外,AI由于运行在服务端,它还能读取更多其他信息,例如事件栈等等。源代码位于lua/server/ai下。
makeReply内部需要就具体情况具体分析。默认的AI中没有对任何一种情况制定策略,因此它除了不断点取消之外什么都不做。新月杀内置了两种AI:TrustAI和SmartAI。 它们继承自AI类,负责计算出实际的结果。其中以TrustAI(托管AI)最为简单,它的功能如下:
需要打出牌时,有的话就打出
需要使用闪、对自己使用无懈、酒、桃时,只要有就使用
除此之外什么都不做
为此,它需要对两种command做出计算:询问使用(AskForUseCard)和询问打出(AskForResponseCard)。 从源代码(lua/server/ai/trust_ai.lua)可以看出,它定义了两个方法分别处理两种command。 而处理方法更是非常简单,对于打出牌,只要有可选的牌就直接选中并确定;对于使用牌,根据data中提示的牌名信息以及data.prompt中提示出的目标信息进行简单判断,并最终按下确定。
由此,我们可以看出,makeReply中是根据command类型的不同,交由AI的 handle<command>
进行更进一步的思考。TrustAI中只管稍微看看然后点点确定就行了,但是SmartAI要考虑的就多了。
8.2. SmartAI的决策思路#
如上所述,单单走到了handleCommand这一步还没完,要做出还算合理的决策,必须要综合更多信息。 在SmartAI中,这样的决策又进一步托付给了各个skill。因为全游戏几乎所有的询问都是由技能产生的, 因此让触发这些询问的技能来自备AI策略也相当合理。SmartAI要做的事情就是把决策进一步转交给技能的AI代码,仅此而已。
既然只要做下放给技能ai这一件事,那么SmartAI怎么找到那样的技能呢?答案是事件栈。首先, SmartAI先检查当前是否处于某个触发技的on_cost环节,如果是的话,那个就认定这个触发技是相关的;否则SmartAI检查事件栈,如果栈顶事件(也就是当前事件)是SkillEffect,那么直接认定找到了相关的skill。
那么问题来了,技能的AI究竟该如何编写相关的策略呢?作为新月杀的老前辈,太阳神三国杀给出了答案, 以标准版突袭为例,它的决策是,考虑偷敌人的手牌,但是在此之前,考虑一下诸葛亮(帮他空城)、 姜维(也是帮他空城以达到觉醒条件)、邓艾(帮他屯田)等等武将,如果他们是队友那么也考虑偷他们的手牌。这种决策自然是一目了然,但是弊端也很明显,那就是如果这么决策的话,那么其实所有偷手牌类的技能全都要如此考虑一下。
当然了,技能本身的自由度很高很高,想怎么写策略是拓展开发者的自由,但是新月杀本身也要维护标准包、标准包卡牌和军争卡牌这几个拓展包,自然也要给出自己的策略实现方案。有鉴于神杀,新月杀提出的方案是量化某某操作的收益,然后其他技能可以联动收益计算,这样就免除了所有偷牌技能都考虑一遍帮队友空城的情况。问题是这样的收益太难界定,中间的思考历程我忘了,总而言之,新月杀的收益计算是基于直接模拟操作之后未来的操作,从操作中计算出收益值。计算出收益之后,程序再适当搜索其他几个操作的收益值,最终选出收益最大的决策方案。
8.2.1. 收益计算器:模拟游戏流程#
在AI中有一套和游戏逻辑代码重复度非常高的游戏逻辑模拟器(倒不如说整个游戏模拟器就是完全贴着拓展包api设计的),它可以基于整套流程中对玩家的数值变动, 计算出某个操作的收益。游戏逻辑模拟器的源码位于lua/server/ai/logic.lua中,其计算收益的核心思路:
待处理
虽然这套收益计算非常蠢,但现在还没到修改它们的时机,后面再来仔细推敲策略吧。 关于如何计算各种收益或许值得和三国杀老玩家们一起讨论很多,目前填的收益值全是我瞎填的。
当游戏逻辑模拟器执行到setPlayerProperty时,完全不修改时机数值,而是根据变化量界定收益 * 一滴血200收益,一护甲150收益,剩下自己看吧有点蠢就是了
当游戏逻辑模拟器执行到移动卡牌时(applyMoveInfo),如此计算收益 * 手牌区获得一张+90,装备区获得一张+110,判定区获得一张-180,私人牌堆获得一张+60 * 失去牌时收益反之
就这样,游戏逻辑模拟器执行着和真实逻辑几乎完全一致的代码,但是在以上两个关键函数做了修改,从而达成了只计算收益不打扰房间的目的。这样一来,收益论计算就可简单的归结为: 首先模拟一段操作,然后读取计算出的收益值。就目前而言,每张卡牌已经实现了默认的收益计算方式, 也就是模拟一次useCard操作的收益。这样一来,卡牌AI中写好供游戏模拟器调用的on_use和 on_effect就能让它正常计算收益了。
第二个问题是其他技能如何影响收益计算,这样才能达到避免做出对仁王盾出黑杀之类的无用操作。这个问题的解决思路就是让游戏逻辑模拟器模拟 trigger 函数,也就是模拟一下触发某个时机。在模拟触发时机中,模拟器会像普通的trigger那样查询一下房间内存在着的
所有触发技,然后计算这样的触发技会不会在当前时机和条件(当然是模拟出来的条件)下对收益计算产生某种影响。显而易见,这样的某种影响自然需要触发技自行编写AI代码。
触发技AI能做出这些影响:第一点,执行某些模拟操作,通过操作来达成收益值的修改; 第二点,那就是直接令收益计算结束(类似于正常逻辑中的结束事件,return true嘛)。 在突袭例子中,如果要考虑空城,那么就写个AI,让模拟器认定自己失去最后的手牌时, 增加某个数值的收益(因为空城本身没有衍生事件,只是有个高防御的状态技;你可以举一反三的联想一下陆逊失去最后手牌时会有摸一张牌的收益);目前还未编写这样的空城收益论,如果想实地查看代码,可以查看仁王盾的AI:当被黑杀杀时,直接结束收益计算。 因为游戏模拟器要到实际改变hp的那一步才产生收益变化,所以直接结束的话自然就不会去考虑造成伤害的收益了。
最后不得不说的是,这样的收益计算也有其局限性。一方面,出于性能原因的考虑,底层限制了模拟的范围,以及模拟中产生事件嵌套时的嵌套层数;另一方面,在流程模拟中还会进一步遇到新的决策,于是AI又要去进行它的思考,对于这样的嵌套思考限制更加严格; 再另一方面,由于流程模拟中不会修改任何实际的状态,如果思考的太远,那么算出的结果会有很大偏差。这些都让收益计算只能局限在未来的少数几个事件,但是也基本上简单够用了。
8.2.2. RequestHandler:模拟玩家面板#
收益计算这一方面算是告一段落,另一个问题是在思考之前如何避免进行复杂的合法性判断,例如某人禁止我们出红色锦囊牌,那么乱击选两张方块自然就无法放箭。 为了免去重复判断合法性的麻烦,我们可以将上一章所述的RequestHandler拿来复用, 直接利用它们模拟点击卡牌、目标、技能等,从而减少那些不必要的可用性考虑。
待处理
其实上一章还没解说RequestHandler的概念以及用法呢。只好todo。 顺便其实我也不知道这玩意怎么用、怎么解说才好,感觉还需修改才能变的好用。
如前一张所言,只有最复杂的(至少UI上存在复杂的逻辑)操作才会利用RequestHandler 将逻辑放在Lua中处理,因此像AskForSkillInvoke、AskForCardChosen这样的简单操作就用不上(也用不了)Handler了,我们主要考虑涉及面板操作的(手牌区等等)。 参见lua/ai/ai.lua中对handler的各种方便封装,我们可以利用它们在ai代码中模拟各种点击操作等。
需要注意的是,只有在合法性判断相当复杂的时候才适合搬出RequestHandler进行判断。 像制衡之类的合法性判断很简单的技能,就不需要去模拟点击之类的操作了,可以大胆的直接算出想要选择的牌并return它们。(这样可能越过合法性判断而你没发现)
8.2.3. 技能编写策略的接口#
所以,一部分command需要使用RequestHandler来模拟面板或者什么东西,另一部分command则没有这样的面板模拟,直接将数据作为函数参数传递。这一块还处于施工中, 但大体上而言,分为以下几种:
think:所有涉及到面板的操作(共四种,Active、使用、打出、出牌阶段)共用该函数。 技能需要自行在内部进行更多判断来辨别出think时自己处于哪个询问。thinkForXXX:其余没有涉及面板,且command格式为askForXXX的处理函数。 这个函数的参数表与askForXXX的参数表一般会很接近。其他的think:如果之后出现了其他的RequestHandler,那么自然也会有特别的think。
这一系列以think开头的函数都需要返回两个值,第一个是答案本身,第二个是这个答案的估计收益值。其中又属 think 最最复杂又最常见不过。它有以下几个衍生方法:
待处理
这些衍生方法是否该存在仍然值得商榷。还要讨论又要讨论
choose_interaction:暂定choose_cards:暂定choose_targets:选出最适合选择的目标以及选它们的收益
对于卡牌的ai而言,还有两个特供方法:
on_use:收益计算专用,模拟卡牌使用时on_effect:收益计算专用,模拟卡牌生效
另一方面,触发技可以编写代码来影响收益计算中对触发时机的判断。编写者只要实现一个方法就行了。
8.2.4. 推荐的决策策略#
如前所述,我们有还算过得去的收益计算器以及一个面板模拟器可以使用,