跳转至内容
  • 0 赞同
    1 帖子
    18 浏览
    jarlyynJ
    简而言之,用户队列,就是指令队列在用户输入框里的实现。 核心部分,之前都是纯代码层与用户无关的基础建筑,到了这里才算得上是上层建筑。 先看代码 代码地址 我们会发现,用户队列,Userqueue,是在Command包下的一个很简单的实现。 用队列相对于指令队列,就是额外的实现了一下的内容 Exect方法,把用户的输入分割为多个指令执行 Do和Wait的默认方法,实现最常见的发送指令和等待时间的功能(其实是Command中相应功能的封装) Register指令,注册新的指令扩展功能 Loop指令,循环执行 很明显,用户队列提供了一个让用户直接调用Command的接口,更重要的是,可以循环执行。 当我们把准备阶段也注册为指令时,可以通过#prepare和#loop循环,形成一个简陋板的任务,一个字面意义上的全自动机器人。 比如,打坐机器人 #prepare|yun recover|dazuo 50|#loop 比如,练功 #prepare|yun recover|wield long sword|lian sword 50|#loop 虽然这种用户队列只能实现简单的,没有复杂任务的机器,但这是真正意义上的全自动机器人了。 甚至可以认为,用户队列的稳定性,等以你机器的主体的稳定性。 因此,用户队列对于主任务系统是一个独立的,迷你的系统。 但对于用户手动操作,以及机器的测试开发来说,还是有很大的意义的。
  • 0 赞同
    1 帖子
    13 浏览
    jarlyynJ
    机器人的代码组织有很多种方式。 我自己的机器人其实经历过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,才能避免内存泄漏
  • 0 赞同
    1 帖子
    14 浏览
    jarlyynJ
    条件指令解决的是复杂Mud配置的问题。 Mud全自动机器人本质是一个典型的后台服务,和后台服务器一样,配置基本是多个纯文(变量)本来进行的。 同时,由于一般的Mud客户端的变量设置都没有语法支持,基本就是纯纯文本。所以会需要一个易写易读的配置格式。 这种情况下,我引入了条件指令的格式。 代码连接 具体格式为 条件1 条件参数1,!条件2>#指令.参数 数据 逗号分隔多个条件,前置感叹号代表取反,必须所有条件都符合。 指令参数数据看具体的实现,先不深入。 具体使用什么条件,就是从上往下依次读,看条件匹配,匹配了,就执行,不继续执行(特殊配置可能继续匹配)。 不匹配看下一条。 这样人脑比较好解读。 同时,为了避免过于复杂的场合难以解读,还引入了可选的分组的概念 分组1:条件1 条件参数1,!条件2>#指令.参数 数据 这样能直观的进行组别判断。 以我最复杂的战斗设置为例 大概是这么个画风 #before #wpon;yun recover; #start yong cuff.jingang;perform unarmed.chang1;perform finger.chao and strike.qimen yun recover perform finger.chao and strike.qimen #block 秦岭 ctype qinling>#apply #before yun recover;$wpon;summon xiao;unwield xiao #start yong cuff.jingang;wield xiao;perform sword.feilong qin and finger.ding qin;get xiao;unwield xiao wield xiao;perform sword.feilong qin and finger.ding qin;get xiao;unwield xiao 注意,这里还引入了block区块的概念 条件本身是有一个全局注册的 大概代码为 //注册maxexp 条件 App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("maxexp", function (data, target) { return App.Data.Player.HP["经验"] <= (data - 0) })) //注册yueli 条件 App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("yueli", function (data, target) { return App.Data.Player.Score["阅历"] >= (data - 0) })) //注册pot 条件 App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("pot", function (data, target) { return App.Data.Player.HP["潜能"] >= (data - 0) })) //注册quest 条件 App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("quest", function (data, target) { let rq = App.Quests.Running return rq && rq.ID == data }))
  • 0 赞同
    1 帖子
    41 浏览
    jarlyynJ
    虽然很多客户端和教程介绍机器时,都是用触发来驱动机器。 但是,要做一个稳定,功能强大的机器,被动的靠出发来驱动明显不可行,必须要依靠全面详尽的数据来运行。 在Mud中,有很多指令会对应获取更新数据。常见比如"hp","score","i","skills"等。 我们一般都会根据这些指令的结果来更新数据。 但是,Mud又是动态的,这些数据往往又容易失效。 做任何动作之前猛刷一通指令明显不科学,浪费服务器资源,还干扰用户使用。在这个情况下,我们肯定是要面对有时效性的数据来进行编码。 也就是说,我们在收集数据时,不光光面对是一个采集的问题。还要把数据是做缓存,处理一个缓存失效的问题。 那以缓存的角度来看看到数据的话,实际是要实现以下几个地方 数据的有效期(ttl) 数据回源(发现过期后主动更新数据) 数据的强制更新(更新数据后更新数据和有效期) 数据强制失效 实现的方法其实有很多,我这里说说我的实现方法。 以我的代码为例 在处理数据时,我使用了这么一个结构 let DefaultExecute = function (check) { return check.Command } class Check { constructor(id) { this.#id = id } #id = "" Interval = 0 #last = 0 Execute = DefaultExecute Command = null WithExecute(fn) { this.Execute = fn ? fn : DefaultExecute } ID() { return this.#id } WithCommand(cmd) { if (typeof (cmd) != "function") { this.Command = function () { App.Send(cmd) } } else { this.Command = cmd } return this } WithInterval(interval) { interval = interval - 0 if (isNaN(interval)) { interval = 0 } this.Interval = interval return this } InCooldown() { return (new Date()).getTime() - this.Interval < this.#last } Reset() { this.#last = (new Date()).getTime() } Force() { this.#last = 0 } } 这个机构很明显。 #last 最后一次更新时间 Interval 重新获取数据的间隔(ttl) Execute,Command 更新数据的函数和指令 #id 缓存的 id,可以制定id进行更新 在使用时,我会以这样的形式来注册一个数据检查(更新)器 let checkerSkills = App.Checker.Register("skills", "skills", 300000) 这是注册一个 id为skills,指令为skills,数据有效期为300秒的检查器。 在匹配skill最后的代码最后调用一下Reset function (result) { checkerSkills.Reset() }) 就能按一定的有效期来保存数据了。 然后由于这个是获取技能数据的,所以我绑定了技能提升的触发事件,在技能重置时强制这个缓存数据失效 //技能升级时重置相关的checker App.BindEvent("core.skillimproved", function () { checkerHPM.Force() checkerSkills.Force() checkerJifa.Force() }) 就能比较有效的处理缓存数据了。 在可能需要处理状态的地方,我加入了检查所有预设检查器的代码 //检查的函数 App.Check = () => { App.RaiseEvent(eventBeforeCheck)//触发检查发送指令 let checks = App.Checker.Check()//获取需要执行的检查 if (checks.length == 0) {//无需检查 App.Next() return } App.PushCommands(//一次性执行所有检查 App.Commands.NewFunctionCommand(function () { checks.forEach(check => { check() }); App.Next() }), App.NewSyncCommand(), ) App.Next() } //准备函数,第一个函数时准备id,第二个时准备的上下文(环境变量) App.Prepare = function (id, context) { App.RaiseEvent(eventBeforeCheck) let checks = App.Checker.Check() if (checks.length == 0) { AfterCheck(id, context) return } App.PushCommands( App.Commands.NewFunctionCommand(function () { checks.forEach(check => { check() }); App.Next() }), App.NewSyncCommand(), App.Commands.NewFunctionCommand(function () { AfterCheck(id, context) }), ) App.Next() } 这样就能确保每次在需要数据进行决策(准备)时,使用的是最新的有效的数据。