服务端：人机出牌逻辑
======================

.. note::

   人机仍在施工中，本文内容仅用来说明目前的思路，随时可能过时。

人机出牌逻辑（AI）是一套应对请求与答复的程序，当需要做出答复的玩家并没有真人玩家\
能前来响应时，程序就要自己代替玩家计算出一个较为合理的方案并返回给Request。

新月杀的目标之一是运行在低端设备上（手机、VPS等），为此人机出牌逻辑无法使用当前\
流行的人工智能方案，算力不够。为此如同大多数民间版三国杀一样，新月杀在AI上的选择\
依然是手动为各种询问编写作答策略。

AI的整体思路
--------------

.. todo::

   画图。

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要考虑的就多了。

SmartAI的决策思路
------------------

如上所述，单单走到了handleCommand这一步还没完，要做出还算合理的决策，必须要综合更多信息。
在SmartAI中，这样的决策又进一步托付给了各个skill。因为全游戏几乎所有的询问都是由技能产生的，
因此让触发这些询问的技能来自备AI策略也相当合理。SmartAI要做的事情就是把决策进一步转交给\
技能的AI代码，仅此而已。

既然只要做下放给技能ai这一件事，那么SmartAI怎么找到那样的技能呢？答案是事件栈。首先，
SmartAI先检查当前是否处于某个触发技的on_cost环节，如果是的话，那个就认定这个触发技是相关的；否则\
SmartAI检查事件栈，如果栈顶事件（也就是当前事件）是SkillEffect，那么直接认定找到了相关的skill。

那么问题来了，技能的AI究竟该如何编写相关的策略呢？作为新月杀的老前辈，太阳神三国杀给出了答案，
以标准版突袭为例，它的决策是，考虑偷敌人的手牌，但是在此之前，考虑一下诸葛亮（帮他空城）、
姜维（也是帮他空城以达到觉醒条件）、邓艾（帮他屯田）等等武将，如果他们是队友那么也考虑\
偷他们的手牌。这种决策自然是一目了然，但是弊端也很明显，那就是如果这么决策的话，那么其实\
所有偷手牌类的技能全都要如此考虑一下。

当然了，技能本身的自由度很高很高，想怎么写策略是拓展开发者的自由，但是新月杀本身也要\
维护标准包、标准包卡牌和军争卡牌这几个拓展包，自然也要给出自己的策略实现方案。\
有鉴于神杀，新月杀提出的方案是量化某某操作的收益，然后其他技能可以联动收益计算，这样就\
免除了所有偷牌技能都考虑一遍帮队友空城的情况。问题是这样的收益太难界定，中间的思考\
历程我忘了，总而言之，新月杀的收益计算是基于直接模拟操作之后未来的操作，从操作中计算出\
收益值。计算出收益之后，程序再适当搜索其他几个操作的收益值，最终选出收益最大的决策方案。

收益计算器：模拟游戏流程
~~~~~~~~~~~~~~~~~~~~~~~~~

在AI中有一套和游戏逻辑代码重复度非常高的游戏逻辑模拟器（倒不如说整个游戏\
模拟器就是完全贴着拓展包api设计的），它可以基于整套流程中对玩家的数值变动，
计算出某个操作的收益。游戏逻辑模拟器的源码位于lua/server/ai/logic.lua中，其计算收益的核心思路：

.. todo::

   虽然这套收益计算非常蠢，但现在还没到修改它们的时机，后面再来仔细推敲策略吧。
   关于如何计算各种收益或许值得和三国杀老玩家们一起讨论很多，目前填的收益值全是我瞎填的。

- 当游戏逻辑模拟器执行到setPlayerProperty时，完全不修改时机数值，而是根据变化量界定收益
  * 一滴血200收益，一护甲150收益，剩下自己看吧有点蠢就是了
- 当游戏逻辑模拟器执行到移动卡牌时（applyMoveInfo），如此计算收益
  * 手牌区获得一张+90，装备区获得一张+110，判定区获得一张-180，私人牌堆获得一张+60
  * 失去牌时收益反之

就这样，游戏逻辑模拟器执行着和真实逻辑几乎完全一致的代码，但是在以上两个关键函数做了\
修改，从而达成了只计算收益不打扰房间的目的。这样一来，收益论计算就可简单的归结为：
首先模拟一段操作，然后读取计算出的收益值。就目前而言，每张卡牌已经实现了默认的收益计算方式，
也就是模拟一次useCard操作的收益。这样一来，卡牌AI中写好供游戏模拟器调用的on_use和
on_effect就能让它正常计算收益了。

第二个问题是其他技能如何影响收益计算，这样才能达到避免做出对仁王盾出黑杀之类的无用\
操作。这个问题的解决思路就是让游戏逻辑模拟器模拟 ``trigger`` 函数，也就是模拟一下\
触发某个时机。在模拟触发时机中，模拟器会像普通的trigger那样查询一下房间内存在着的
所有触发技，然后计算这样的触发技会不会在当前时机和条件（当然是模拟出来的条件）下\
对收益计算产生某种影响。显而易见，这样的某种影响自然需要触发技自行编写AI代码。

触发技AI能做出这些影响：第一点，执行某些模拟操作，通过操作来达成收益值的修改；
第二点，那就是直接令收益计算结束（类似于正常逻辑中的结束事件，return true嘛）。
在突袭例子中，如果要考虑空城，那么就写个AI，让模拟器认定自己失去最后的手牌时，
增加某个数值的收益（因为空城本身没有衍生事件，只是有个高防御的状态技；你可以\
举一反三的联想一下陆逊失去最后手牌时会有摸一张牌的收益）；目前还未编写这样的\
空城收益论，如果想实地查看代码，可以查看仁王盾的AI：当被黑杀杀时，直接结束收益计算。
因为游戏模拟器要到实际改变hp的那一步才产生收益变化，所以直接结束的话自然就\
不会去考虑造成伤害的收益了。

最后不得不说的是，这样的收益计算也有其局限性。一方面，出于性能原因的考虑，底层\
限制了模拟的范围，以及模拟中产生事件嵌套时的嵌套层数；另一方面，在流程模拟中\
还会进一步遇到新的决策，于是AI又要去进行它的思考，对于这样的嵌套思考限制更加严格；
再另一方面，由于流程模拟中不会修改任何实际的状态，如果思考的太远，那么算出的结果\
会有很大偏差。这些都让收益计算只能局限在未来的少数几个事件，但是也基本上简单够用了。

RequestHandler：模拟玩家面板
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

收益计算这一方面算是告一段落，另一个问题是在思考之前如何避免进行复杂的合法性\
判断，例如某人禁止我们出红色锦囊牌，那么乱击选两张方块自然就无法放箭。
为了免去重复判断合法性的麻烦，我们可以将上一章所述的RequestHandler拿来复用，
直接利用它们模拟点击卡牌、目标、技能等，从而减少那些不必要的可用性考虑。

.. todo::

   其实上一章还没解说RequestHandler的概念以及用法呢。只好todo。
   顺便其实我也不知道这玩意怎么用、怎么解说才好，感觉还需修改才能变的好用。

如前一张所言，只有最复杂的（至少UI上存在复杂的逻辑）操作才会利用RequestHandler
将逻辑放在Lua中处理，因此像AskForSkillInvoke、AskForCardChosen这样的简单操作就\
用不上（也用不了）Handler了，我们主要考虑涉及面板操作的（手牌区等等）。
参见lua/ai/ai.lua中对handler的各种方便封装，我们可以利用它们在ai代码中模拟各种点击操作等。

需要注意的是，只有在合法性判断相当复杂的时候才适合搬出RequestHandler进行判断。
像制衡之类的合法性判断很简单的技能，就不需要去模拟点击之类的操作了，可以大胆的\
直接算出想要选择的牌并return它们。（这样可能越过合法性判断而你没发现）

技能编写策略的接口
~~~~~~~~~~~~~~~~~~~

所以，一部分command需要使用RequestHandler来模拟面板或者什么东西，另一部分\
command则没有这样的面板模拟，直接将数据作为函数参数传递。这一块还处于施工中，
但大体上而言，分为以下几种：

- ``think`` ：所有涉及到面板的操作（共四种，Active、使用、打出、出牌阶段）共用该函数。
  技能需要自行在内部进行更多判断来辨别出think时自己处于哪个询问。
- ``thinkForXXX`` ：其余没有涉及面板，且command格式为askForXXX的处理函数。
  这个函数的参数表与askForXXX的参数表一般会很接近。
- 其他的think：如果之后出现了其他的RequestHandler，那么自然也会有特别的think。

这一系列以think开头的函数都需要返回两个值，第一个是答案本身，第二个是这个答案的\
估计收益值。其中又属 ``think`` 最最复杂又最常见不过。它有以下几个衍生方法：

.. todo::

   这些衍生方法是否该存在仍然值得商榷。还要讨论又要讨论

- ``choose_interaction`` ：暂定
- ``choose_cards`` ：暂定
- ``choose_targets`` ：选出最适合选择的目标以及选它们的收益

对于卡牌的ai而言，还有两个特供方法：

- ``on_use`` ：收益计算专用，模拟卡牌使用时
- ``on_effect`` ：收益计算专用，模拟卡牌生效

另一方面，触发技可以编写代码来影响收益计算中对触发时机的判断。编写者只要实现一个方法就行了。

推荐的决策策略
~~~~~~~~~~~~~~~

如前所述，我们有还算过得去的收益计算器以及一个面板模拟器可以使用，
