深入浅出制作全自动Mud机器人-指令队列
-
机器人的代码组织有很多种方式。
我自己的机器人其实经历过3个阶段
- helllua ,使用的是回调链
- pkuxkx.noob ,试图尝试使用状态机,后来切换到指令队列
- newhelljs 精简的指令队列
这里就说下我目前使用的指令队列结构。
老规矩,线上代码
什么是指令队列
其实指令队列在Rts游戏以及衍生出的Moba游戏里是很常见的。
就是在war3,starcraft或者dota2里,按住shift,依次发布多个指令,这些指令就会进入队列,一个一个执行完。
这很策略,巧的是,写Mud全自动机器人也是个策略活。很自然的,我就引入了指令这个概念。
指令的实现
指令其实很简单
class Command { constructor(name, data) { this.Name = name this.Data = data if (module.Debug) { this.Stack = (new Error()).stack } } Context = {} Name = "" Stack = "" Data = null OnEvent = null }主体就是Name和Data,以及后续会被队列引入的上下文Context。
那么执行的代码呢?
执行的代码是分离开的概念,放在指令执行器里.
执行其的作用是传入队列和 指令,然后初始化对应指令功能的模块
比如
module.ExecutorFunction = function (commands, running) { running.OnStart = function (arg) { running.Command.Data() } }为什么把指令和执行其分开呢?
因为这样指令队列就是纯数据不包含代码,方便Debug和进行快照/恢复
基础队列
很明显,我们的队列,就是一个Command数组
我们可以对数组进行Insert和Append操作。
可以通过Next操作弹出第一个指令执行
然后,我们还有一个压入指令组的功能(PushCommands)
就是冻结当前队列,执行新队列,可以Pop中断继续执行前一个队列,同时切换队列的上下文(Context, 不同指令之间共享的运行时数据)
这个结构下,队列Quesus,本质是一个 Command的二位数组
控制结构
当我们有了多层的数组后,我们就可以实现简单的控制结构了。
我定义了以下控制接口
- Push/PushCommands 压入Queue
- Pop 放弃当前Queue剩余内容,返回之前的调用队列
- Snap/Rollback 做快照/回滚快照。由于 Command是纯数据结构,就是克隆一份Queue出来
- Append/Insert 在当前队列的最前端/后端插入指令
- Fail 报错失败,会废弃所以没有定义FailCommand的队列,直到队列全空。FailCommand相当于一般语言的try..catch
- FinalCommand 就算Pop中断也会执行的指令
意外应对机制
本质来说,指令队列是预先定义蓝图,在顺利的情况下完全实行。
但实际上,往往会有意外发生,需要跳过当前指令和指令队列强行执行其他的。
这时候,从逻辑上我们必须提供一个快照和回滚的机制,确保意外处理后有可能继续执行当前工作的方法。
这就是我对指令队列打的补丁。
相当于 Mud机器中不止有一个主逻辑,只是平时其他的逻辑都是蛰伏状态。当意外发生时,副逻辑介入,先保留住逻辑现场,处理完之后,再根据实际情况决定是否要恢复主逻辑运行(实际很少这样处理)
异步与同步
实际上Command本质是一个轻量级的Promise。是一个对未来的期望,是异步(在将来)执行的代码。
所以,很多时候,我们需要和实际指令进行同步,就是等待Mud服务器返回一个特殊的返回,确保之前的指令都已经执行完毕了。
我一般会用nobusy和sync 的Command来实现这个功能
典型代码
以我的炼药采买为例
Lianyao.BuyAll = (cmds) => { $.PushCommands( $.To("65"), $.Do(cmds.join("\n")), $.Do("i"), $.Wait(1000), $.Sync(), $.Function(Lianyao.Make) ) $.Next() }这个能很明显的看出队列的用法
规划一堆指令,Sync去订都执行完毕了,然后继续后续。
最后$.Next释放控制权
指令队列的挑战
指令队列或者其他纯代码逻辑控制的驱动方式,最大的挑战其实是内存泄漏。
比如如果要循环执行指令的话,不停PushCommands会堆很多很多层空队列,直到爆栈为止。
这是必须通过在最外层Insert指令,而不是PushCommands,才能避免内存泄漏
-
J jarlyyn 在 引用了 此主题