跳转至内容
  • 0 赞同
    1 帖子
    14 浏览
    jarlyynJ
    在开始制作一个Mud机器人的移动模块之前,首先我们先要解决移动确认,也就是判断成功进入一个房间的问题。 稍微延展点的话,就是解析当前房间的信息 确定房间信息开始 抓取房间名和其他信息 抓取房间描述 抓取房间出口 抓取房间内对象 确定房间信息结束 本文章以我newhelljs的解析房间代码为例 代码地址 房间信息开始 一般房间信息开始未必是一个很明确的信号。 部分mud可能对房间有特殊的格式。但很多Mud单纯就是一个顶格不带特殊标点的短剧。 所以我在处理房间时是分为两个部分 第一是普通房间,比如 ^[^,。!:.『』【】…“”??>.]{2,10}$ 然后这里我使用了一个Filter的概念,就是不直接抛事件,而是在进行了预处理后,符合条件时才抛事件。对应的代码是: App.Engine.SetFilter("core.normalroomname", function (event) { let words = App.History.CurrentOutput.Words if (words.length == 1 && words[0].Color != "" && words[0].Bold == true) { App.RaiseEvent(event) } }) 进行了简单的视觉判断,单色,非普通色,加粗。 除了这个通用标准外,有特殊的出发可以直接抛房间名事件。 在这里很明显,我是一个可能判断,并不能保证100%匹配。 所以我并没有确定出现疑似房间名的信息就进入房间。 出现疑似房间名我只是把已经抓取的房间描述清空,记录最后一个房间名,直到确定进入房间在把这些转化为当前房间信息。 过滤干扰项 我在代码里把所有季节相关的干扰项都记录在了"data/natures.txt"文件里,并进行了排除。 这是为了抓取数据做快照而作的准备。 如果你不需要抓取描述,可以不做这个处理。 出口信息 大部分Mud,出口信息都是比较统一,标准的,所以可以作为真正确认进入房间的信号 我的代码里,就是在抓取到出口信息后,确认之前抓起的房间名和描述有效,开始抓取房间对象列表。 如果你玩的Mud的代码比较复杂,这里可能有更多的处理。 对象列表 我这里主要是将对象可能的几个形态都列了出来,统一插入Room对象的 Object列表中。 遇到的第一个不符合条件的行,就确认房间结束。 这个主要还是看wiz怎么处理,有时候还会需要做特殊的处理。 App.BindEvent("core.onexit", App.Core.Room.OnExit) let matcherOnHeal = /^ (\S{2,8})正坐在地下(.+)。$/ let matcherOnYanlian = /^ (\S{2,8})正在演练招式。$/ let matcherOnObj = /^ ((\S+) )?(\S*[「\(].+[\)」])?(\S+)\(([^\(\)]+)\)( \[.+\])?(( <.+>)*)$/ //处理得到出口之后的信息(npc和道具列表)的计划 var PlanOnExit = new App.Plan(App.Positions.Connect, function (task) { task.AddTrigger(matcherOnObj, function (trigger, result, event) { let item = new objectModule.Object(result[4], result[5], App.History.CurrentOutput). WithParam("身份", result[2]). WithParam("外号", result[3]). WithParam("描述", result[6] || ""). WithParam("状态", result[7] || ""). WithParam("动作", "") App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddTrigger(matcherOnHeal, function (trigger, result, event) { let item = new objectModule.Object(result[1], "", App.History.CurrentOutput). WithParam("动作", result[2]) App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddTrigger(matcherOnYanlian, function (trigger, result, event) { let item = new objectModule.Object(result[1], "", App.History.CurrentOutput). WithParam("动作", "演练招式") App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddCatcher("line", function (catcher, event) { let output = event.Data.Output if (output.length > 2 && output.startsWith(" ") && output[2] != " ") { return true } //未匹配过的行代表npc和道具结束 return event.Context.Get("core.room.onobject") }) }, function (result) { if (result.Type != "cancel") { if (App.Map.Room.Name && !App.Map.Room.ID) { let idlist = App.Map.Data.RoomsByName[App.Map.Room.Name] if (idlist && idlist.length == 1) { App.Map.Room.ID = idlist[0] } } App.RaiseEvent(new App.Event("core.roomentry")) } }) 忽略信号 一般来说,会有两种情况需要忽略移动确认的信号。 第一种是MUD的干扰。Mud的Wiz可能能会设置一个类似进入房间的干扰选项。这个需要在代码里进行排除,具体实现要看Mud的设置。 第二种是主动的Look。 我在我的移动库做了特殊处理 EnterNewRoom(room) { if (!room) { room = new Room() } let oroom = this.Room this.Room = room if (oroom.Keep) { if (oroom.ID) { this.Room.ID = oroom.ID } this.Room.Keeping = true } this.Room.Keep = false return this.Room } OnWalking() { if (!this.Room.Keeping) { this.Position.StartNewTerm() } if (this.Move != null) { this.Move.OnWalking(this) } 在Room有Keep属性时,不做彻底的清理处理,并不会触发对应的移动动作。 封装事件 由于Room处理是一个很复杂的工作。 所以正常情况下,有一个专门的Room处理函数进行处理,处理完毕后抛出事件通知需要后续操作的模块。 这样才能最大的程度的解耦合,并保证房间处理信息的内聚。 不做切割的话,很容易随着房间信息格式的调整,把整个代码搅乱。
  • 深入浅出制作全自动Mud机器人-同步

    Script脚本 全自动 机器人 代码范例
    1
    0 赞同
    1 帖子
    23 浏览
    jarlyynJ
    同步是和异步相对的概念,一般来说,编码里的同步和异步的区别是在于是否阻塞。 同步的代码会阻塞并等待任务的返回结果。 异步的代码一般只发起任务,不关心任务是否完成,可以通过回调/通道之类的方法获取处理结果。 在我们的机器人中,一般会用异步的方式抛出一系列的指令,然后需要一个同步的指令,阻塞任务队列,保证不会被其他的信息污染,来进行更详细的指令判断。 比如,我newhelljjs中有一个炼丹模块,代码如下 $.PushCommands( $.To("1387"), $.Do("give cao yao to xiao tong"), $.To("1389"), $.Do("i"), $.Sync(), $.Plan(PlanLiandan), $.Sync(), $.To("1388"), $.Ask("yao chun", "炼丹"), ... ) 就很明显,用了两个$.Sync的同步指令,确保在新指令执行时,之前的指令已经处理完毕。 比如第一个$.Sync(),就是确保i指令已经从Mud服务端获取到了完整的回复,更新了角色的当前物品清单信息。 选择合适的指令 首先,我们的代码会需要选择一个合适的同步指令。这个指令要满足以下的要求: 什么时候都能使用 返回固定,没有消耗 对服务器压力低 平时不会使用,避免误触发 那么,对于没有提供对应低消耗指令的mud,我们一般会找这样的指令: 1.已经废弃的指令,使用时只有废弃提示。 2.有特殊格式要求的,不带参数会报错的。 注意,一个正经的机器人我们还是要注意服务器消耗的。不光光是公德心的问题,毕竟写的太丑的机器人也丢自己脸是不? 另外,除了同步指令,一般建议再找一个判忙的指令,要求和同步的指令类似,区别是在忙和不忙得情况下,回复会有不同。 毕竟忙(busy)也是很多mud的重要状态之一。而且很多时候,判忙也能代替同步指令的作用(更复杂的同步)。 合理的使用 使用同步/忙指令主要有以下特殊点: 避免连续重复使用,容易导致信号失真,特别是有可能进入状态重置的情况。 2.在无法避免信号失真的情况下,要保证机器能在短暂的失效后能重回正轨,有一定的容错性。 3.对于复杂场景,要有个统一的失效机制。也就是不要在等到同步信号之前就开始执行指令,在使用了同步机制后,要统一的在同步完成后再开始小一步。 举个例子,由于我的机器采用的是预期管理的模型驱动的,我特地建立一个Response的作用范围,然后效果如下: let PlanTimes = new App.Plan( App.Positions["Response"], (task) => { task.AddTrigger(matcherTimes, (tri, result) => { if (result[1] == San.Data.WeaponName) { task.Data = result[2] } }) App.Send(`l ${San.Data.Weapon}`) App.Sync() }, (result) => { if (result.Task.Data) { San.Data.Times = App.CNumber.ParseNumber(result.Task.Data) App.HUD.Update() } App.Next() } ) 利用我的期望失效的机制,强制在App.Sync()后,接收到同步信号时,通过Task.Data来判断结果和下一步的动作,避免会在同步信号前再发送一个同步指令,造成混乱。 避免滥用同步指令 同步指令一般只用在发送多个指令后进入不可预期的状态时的确认。 很多有明确预期的指令,比如移动,遍历,不应该以来同步指令,避免对服务器的不必要的压力和效率降低。 最典型的就是判断当前房间移动结束,或者房间的物品列表。 尽量使用 格式的变化(比如第一个出现的非房间物品)来判断。 比如 App.BindEvent("core.onexit", App.Core.Room.OnExit) let matcherOnHeal = /^ (\S{2,8})正坐在地下(.+)。$/ let matcherOnYanlian = /^ (\S{2,8})正在演练招式。$/ let matcherOnObj = /^ ((\S+) )?(\S*[「\(].+[\)」])?(\S+)\(([^\(\)]+)\)( \[.+\])?(( <.+>)*)$/ //处理得到出口之后的信息(npc和道具列表)的计划 var PlanOnExit = new App.Plan(App.Positions.Connect, function (task) { task.AddTrigger(matcherOnObj, function (trigger, result, event) { let item = new objectModule.Object(result[4], result[5], App.History.CurrentOutput). WithParam("身份", result[2]). WithParam("外号", result[3]). WithParam("描述", result[6] || ""). WithParam("状态", result[7] || ""). WithParam("动作", "") App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddTrigger(matcherOnHeal, function (trigger, result, event) { let item = new objectModule.Object(result[1], "", App.History.CurrentOutput). WithParam("动作", result[2]) App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddTrigger(matcherOnYanlian, function (trigger, result, event) { let item = new objectModule.Object(result[1], "", App.History.CurrentOutput). WithParam("动作", "演练招式") App.Map.Room.Data.Objects.Append(item) event.Context.Set("core.room.onobject", true) return true }) task.AddCatcher("line", function (catcher, event) { let output = event.Data.Output if (output.length > 4 && output.startsWith(" ") && output[4] != " ") { return true } //未匹配过的行代表npc和道具结束 return event.Context.Get("core.room.onobject") }) }, function (result) { if (result.Type != "cancel") { if (App.Map.Room.Name && !App.Map.Room.ID) { let idlist = App.Map.Data.RoomsByName[App.Map.Room.Name] if (idlist && idlist.length == 1) { App.Map.Room.ID = idlist[0] } } App.RaiseEvent(new App.Event("core.roomentry")) } }) 是通过出现的行不是以4个空格开头(固定格式)来判断的。
  • 深入浅出制作全自动Mud机器人-战斗系统

    Script脚本 全自动 机器人
    1
    0 赞同
    1 帖子
    15 浏览
    jarlyynJ
    相对而言,我玩过的Mud中,战斗系统都是最简单的。 一般就是一个Timer搞定,复杂点就是带策略的Timer。 这倒不是Mud不能做复杂的战斗系统,反而是体现形式的制约。 毕竟Mud是纯文字展示的,一战斗就是刷刷的刷屏,想要复杂点就直接放弃手动玩的可能了。 所以,对于我而言,一般战斗都是timer实现,然后加上一些常用的变量判断。比如: Duration 持续时间 CType 战斗类型 Tag 一些标签,比如是否是偷袭之类 Life/Neili 当前属性 CQuest 当前任务 由于战斗的特殊性,我还引入了Block,就是一个配置块对应一个战斗类型。 举一个简单的配置为例 #before yun recover;yun regenerate;#wpon #start perform finger.chao and strike.qimen yun recover perform finger.chao and strike.qimen #block mq ctype mq>#apply #before yun recover;yun regenerate;#wpon #start perform finger.ding twice #start perform finger.chao and strike.qimen yun recover perform finger.ding twice perform finger.chao and strike.qimen #block 巫妖 ctype xuemo,ctag sklich>#apply #before yun recover;yun regenerate;#wpon #start perform finger.ding skeleton lich twice yun recover #start perform finger.ding skeleton lich twice perform finger.ding skeleton lich twice perform finger.chao and finger.ding skeleton lich perform finger.chao and strike.qimen #block 丁一 ctype xuemo,ctag boss>#apply #before yun recover;yun regenerate;#wpon; #start perform finger.ding ding yi twice yun recover perform finger.chao and finger.ding ding yi perform finger.chao and strike.qimen #block qinling ctype qinling>#apply #start perform finger.ding qin shihuang twice;perform finger.chao and finger.ding qin shihuang 对应的解释: 普通战斗,一个chao+qimen解决 战斗类别为mq(也就是师门任务),使用mq block 具体就是先buy一下,然后chao+qimen,相对安全点 战斗类型为xuemo,战斗标签为sklick的,使用 巫妖 block 优先busy,优先攻击skeleton lich 战斗类型为xuemo,战斗标签为boss的,使用 丁一 block 优先攻击丁一 战斗类型为qinling的 busy+输出秦始皇 然后只有攻击的一次pfm 总体来说,由于Mud表现形式的缺陷,很多Mud的战斗系统之需要一个基于Timer的复杂配置就能完成了。
  • 0 赞同
    1 帖子
    37 浏览
    jarlyynJ
    任务编排和用户队列其实是同一生态位,类似的上层建筑。 任务编排和用户队列的区别是 任务编排是宏观战略层面的,用户队列是微观战术层面的。 任务编排是面向多行文字变量的,用户队列是面向单行用户命令行输出的。 任务编排属于长期任务有与UI的交互,用户队列是临时任务基本与UI无交互。 任务定义 任务Quest,其实是一个很轻的元素。 class Quest { constructor(id) { this.ID = id } InCooldown() { return (new Date()).getTime() < this.CooldownTo } Cooldown(interval) { this.CooldownTo = (new Date()).getTime() + (interval ? interval : 0) } CooldownTo = 0 ID = "" Name = "" Desc = "" Intro = "" Help = "" Group = "" Start = null GetReady = DefaultGetReady OnHUD = DefaultOnHUD OnSummary = DefaultOnSummary OnReport = DefaultOnReport } 可以看到,除了Cooldown相关的,以及GetReady和Start两个入口,全都是和界面交互的内容。 任务编排 在理解了任务是什么时候,我们接着看任务编排。 任务编排是通过条件指令格式,在任务变量中(newhelljs为了方便使用定义了多个任务变量)设置的规划。 任务会从上向下,依次判断 如果任务不符合条件指令的条件,则跳过 如果任务在冷却中,则跳过 如果所有任务都在冷却,等1秒重头循环 典型的任务编排,可能是把收益最高的,冷却最常的任务编排在最前。 比如 lgt qinling mq 就是依次执行 灵感塔(一天一次) 秦岭(2分钟一次) 师门任务(无冷却) 当然,有时候也会用条件变量分阶段执行任务 比如新人任务 maxexp 2000>>tiejiang maxexp 10000>>peiyao !yueli 20>>beiqi maxexp 29999>>letter !yueli 2000>>beiqi maxexp 100000>>fish quit 这个编排,是依次执行 经验不到2000做铁匠任务 经验不到10000做配药任务 (经验超过10000时)阅历不足20就做背齐任务 经验不到30000(阅历超过20)就做送信任务 阅历不足2000(经验超过30000)时继续做备齐 经验不到100000(阅历超过2000)时,钓鱼 (阅历超过2000,经验超过100000)都做完了,下线喽 任务冷却 任务的核心其实就是冷却,就是有冷却的函数。理论上,只靠冷却就能实现主要的任务编排工作。 在任务实现时,可以在接任务或者完成任务时,调用Quest.Cooldown(xxxx)来为任务设置冷却。 如果有任务联动,可以清楚别的任务的冷却时,也可以从全局注册函数Quests中获取对应变量,进行Cooldown(0)清零。 任务预分配 这是我在实际使用时,引入的新概念。 具体来说,就是由于任务是基于编排模式运行的。 所以,当前任务执行完后,下一个任务是否还是当前任务是不可知的。 这时候我们可以预分配一下下一个任务,调用下一个任务的GetReady,如果下一个任务 Ready了,返回了入口函数,同时ID和当前任务不一样,我们就可以执行当前任务的清理工作。 典型的就是师门任务如果要换任务了,就不再等信接下一个师门了 详见mq任务的mq.CanAccept方法 let ready = App.Quests.GetReady() if (ready && ready.RunningQuest && ready.RunningQuest.ID != Quest.ID) { return false } 任务编排带来的新问题 任务编排,本身就是为了自由度,自由扩展,解决 怎么才能完成更复杂的任务,获得更好的回报? 这个问题 同样的,也会引来新的复杂度 就是既然使用任务编排时我们一定会倾向于细化多分任务 那其他的相关设置怎么办?不如不同任务中不同的战斗设置? 所以,我们在配置时,不得不对所有相关的设置都引入条件指令格式。 增加了配置的复杂度。 鱼和熊掌不可兼得,莫过于此。
  • 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 帖子
    14 浏览
    jarlyynJ
    Mud机器人有一种很常见的开发方式,就是 我预期 未来服务器可能会返回N种回复,根据不同的回复执行不同的代码。 以mush为例,最基本的就是设置一组触发器,预期进行时,开启触发组,有任何一个触发命中时,关闭本组,继续执行。 这种模式下主要的课题是失效管理,也就是什么场景下,会结束预期。 我在newhelljs中引入了 一个叫做Task/Plan的概念,对这种操作进行优化 代码地址 具体来说,我定义的预期是这样的。 在某种状态下,一定时间内,预期有N种触发或者事件可能会发生。 触发后默认预期会结束,除非显示的保活。 首先,我定义了一个预期的有效范围。 根据我的恶趣味,我起了个很蛋疼的名字叫Committee(委员会) 委员会里有很多个职位(Position) 每个职位会有Term任期。 所有的预期(task/plan)是挂在任期Term上的。 任期是一个抽象概念,比如连接,房间,任务,战斗。 当有新的连接/进入新房间/开始新人物/进入或离开战斗后,任期就会自动结束。 这样保证了预期不会泄漏长期存在。 比如我在我战斗模块的开始和结束战斗的代码 Start(id, data) { this.Position.StartNewTerm() this.Data = data this.Target = id ? id : "" this.StartAt = (new Date()).getTime() this.Position.AddTimer(this.Interval, () => { this.Ticker(this) }) this.Ticker(this) this.Plan.Execute() this.Combating = true return this } Stop(reason) { if (!this.Combating) { return } let onstop = this.OnStop this.Position.StartNewTerm() onstop(this, reason) this.Target = "" this.Combating = false this.Data = null } 会使用this.Position.StartNewTerm()来更新任期(所有相关的触发/计时器失效) 当计时器失效时,会Task的结算函数,把Task结束的原因TaskResult传入,这样就能根据不同的结果进行判断了。 至于Plan,其实就是一个Task的工厂函数,调用Plan就能常见一个新的Task。 以代码为例 let matcherEnter = /^你连线进入.+。$/ let matcherReenter = /重新连线完毕。$/ let matcherTooFast = /你距上一次退出时间只有.+秒钟,请稍候再登录。$/ let matcherTooFast2 = /你不能在.+秒钟之内连续重新连线。$/ //登录的计划 var PlanOnConnected = new App.Plan(App.Positions.Connect, function (task) { task.AddTrigger(matcherEnter).WithName("enter") task.AddTrigger(matcherReenter).WithName("reenter") task.AddTrigger(matcherTooFast).WithName("toofast") task.AddTrigger(matcherTooFast2).WithName("toofast2") task.AddTimer(5000).WithName("timeout") }, function (result) { switch (result.Type) { case "trigger": switch (result.Name) { case "enter": App.RaiseEvent(new App.Event("core.entermud", false).WithType("system")) App.RaiseEvent(new App.Event("core.relogin", false).WithType("system")) break case "reenter": App.RaiseEvent(new App.Event("core.entermud", true).WithType("system")) App.RaiseEvent(new App.Event("core.reconnect", false).WithType("system")) break case "toofast": case "toofast2": if (App.Core.Connect.Callback) { App.Core.Connect.Next = (new Date()).getTime() + 10000 Note("10秒后重试") } break } break case "timer": if (App.Core.Connect.Callback) { App.Core.Connect.Next = (new Date()).getTime() + 10000 Note("10秒后重试") } break } }) 就是一个很明显的,高内聚的,预期的组合 当前连接有效 有个不同Name的文字出发 有5秒的timeout 结束函数里通过switch语句,对不同类型的结果进行处理,抛出时间或者进行重连。 如果在这当中意外断线,由于有Connect的Position,会直接失效,不会在下次连接时继续触发。
  • 深入浅出制作全自动Mud机器人-事件总线

    Script脚本 机器人 全自动 架构
    1
    0 赞同
    1 帖子
    13 浏览
    jarlyynJ
    事件系统是一种很常见的代码结构。从web开发,GUI软件制作到各种系统,事件系统都是重要的组成部分。 事件总线是指将所有的事件的抛出与监听都挂在一个独立的类(EventBus或者EventBridge)上。 事件系统是天然的解耦合工具,底层代码抛出事件,处理代码接受事件,让人极容易的写出互相之间不依赖的代码。 写机器,自己嵌入一套事件系统是很顺理成章的事情,甚至某些客户端会在客户端内部实现一个全局的事件系统。 但是,就如同《人月神话》中提到过“没有银弹”。极度的自由,很容易带来极度的混乱。滥用事件很容易带来代码组织架构上的混乱,虽然解耦合了,但依然维护苦难。 毕竟我们强调解耦合是为了优化结构,降低维护难度。低耦合还有下半句高内聚。事件系统虽然实现了低耦合,但在内聚上做的一塌糊涂,必须极为注意使用。 事件的处理要有流向性 处理事件最怕的是循环调用,处理程序互相之间调用,织成一张网,如同乱成一团的线头一样,让人无法入手。 从我的角度,我一直推崇代码的架构要分层,比如我使用的一种架构 代码之间,要有严格的上下级关系,调用原则,避免相互依赖,依靠各种依赖注入/装饰类/代理类来实现反向的交互。 这种情况下,我们要有自我约束,事件的监听函数的调用过程,本质应该是一个水向下流,或者水蒸汽向上飘的单向过程。 事件的生产方和事件的消费方要有严格的上下关系。 这样事件的使用就会有头,有尾,能够梳理。 事件应该是业务的封装而非调用的信号 不同系统使用事件要解决的问题是不一样的。 对于Mud来说,事件应该是对实际发生的业务的抽象与封装 比如新的文字行,比如断开连接和建立连接,比如用户的点击操作等。 事件的生产方本质是把各种现实意义发生的元素,抽象成代码,封装成同一的格式,交给流水线。 不应该觉得为了调用一个处理函数,而抛一个事件。抛事件时一定要明确,很可能没有代码接受事件,或者有乱序的多个代码会接受事件。 如果需要明确的调用代码,不应该使用消息,应该使用显性的注册(依赖注入)机制,调用注册点中注册的代码,做明确的显性的调用。 事件处理函数不要抛出事件。 完整的说,事件处理函数,不要在同一个总线(bus,事件的监听系统)上抛出一个不同组的事件。 因为,每个事件,都会有0个或者多个处理函数,在这些处理函数上再抛出事件,会无法确定是否有循环调用,顺序问题以及依赖问题。 我认为唯一适合事件再抛出事件的,是对事件细化扩展为同组的子事件。 比如登陆成功事件,可以细化为 首次登陆 和 重新登陆事件 这就是同组的事件,(任意登陆/首次登陆/重新登陆) 监听程序会明确的监听其中一个,而不会每个都监听。 其他情况我不建议事件再触发时间,混乱度会字数成长。 总结 事件总线系统,是Mud机器人中必不可少的。 事件总线系统,极为强大。 事件总线系统,过于强大,容易混乱,必须有原则有策略的去规范使用。
  • 深入浅出制作全自动Mud机器人-准备阶段

    Script脚本 全自动 机器人
    1
    0 赞同
    1 帖子
    9 浏览
    jarlyynJ
    我觉得,全自动机器人本质是对人在Mud中行为的归纳,也就是常说的抽象。 抽象出的行为模式可能有很多种。 对于我而言,比较擅长的是 信息采集->准备阶段(状态重置)->任务执行 这样一个模型。 所以,继状态采集后,最重要的就是准备阶段。 什么是准备阶段? 准备阶段,粗泛点来说,就是通过不同的准备步骤将角色维护到一个Ready的状态。 通俗来说,其实就是一次角色状态的慢重启。 对,就是电脑出问题了先重启那个意思。 机器人的复杂性很大一部分来自于情况的复杂性。 所以,如果我们能在任务执行前,先进入一个统一的状态,类似修电脑前先重启一下,就能更容易的用更简单的代码来进行维护。 因为只要考虑一种初始状况就可以了。 怎么进行准备阶段。 对于一般机器的准备阶段,我在主逻辑部分也说了,就是一个if队列 对角色的某个属性定一个标准,检查是否符合这个标准 如果不符合这个标准,就执行相应的代码,然后重头开始进行检查。 如果符合这个标准,则当前标准通过,检查下一个标准 如果所有的标准都符合了,准备结束,状态已经重置,进入执行阶段。 所以我们提炼(抽象)一下,就是 我们有几个模块,有先说顺序。 模块有一个Check函数。 模块有一个 执行函数,Check没过就去执行。 对所有的模块可能会有个统一的环境(上下文),作为检查的一些具体细节。 我在newhelljs里就是设置了一个Proposal(提案)类,核心就是有一个Submit(提交)方法 https://github.com/hellclient-scripts/newhelljs/blob/main/script/helllibjs/proposals/proposals.js 所有的准备就是提交一个个的提案,如果提案通过,则返回要执行的方法,去执行。提案每没过,返回一个空,继续执行下一个提案 怎么组织准备阶段的步骤。 我在newhelljs里准备了一个全局注册的容器Proposals,把每个Proposal按id注册进行。这样在执行准备时可以通过一个字符串id数组就制定要进行的提案了。 同时准备了一个提案组的类型,把多个提案绑成一个新提案,比如common,commonwithstudy这样调用和维护都轻松的多。 怎么验证准备的效果 整个全自动Mud稳定的基础其实就是准备阶段。准备阶段大部分情况下是整个机器最复杂的部分,机器架构的很重要的一部分就是把复杂性统一放在准备阶段,这样任务部分就很轻很简单了。 同样,准备是一个很好的测试机器人稳定性的工具。 我们可以做一个空循环 信息采集->准备阶段->等一秒 空跑一段时间,就能验证整个机器人底层框架的稳定性,健壮性,以及是否有内存泄漏之类的问题了。
  • 深入浅出制作全自动Mud机器人-主逻辑

    Script脚本 全自动 机器人 架构
    1
    0 赞同
    1 帖子
    15 浏览
    jarlyynJ
    对于一个机器人来说,并不需要一个主逻辑,完全可以多套逻辑并行作用。 但对于写机器人的你我来说,大脑还是更适合单线程的模式。所以在机器的合适的地方维护一套主逻辑,即降低开发的难度,又增加维护的效率,个人认为是性价比很高的一种方式。 主逻辑的入口 正常情况下,主逻辑是在正常运行时不调用的调用的。所以,在写主逻辑时,从什么地方进入,调用主逻辑,是我们首先要考虑的点。 定时器入口 其实,通过定时器/心跳来调用主逻辑,在我看来是正道。市面上的各种机器人,自动驾驶,本质也是在每个tick计算当前的状态和应该进行的操作,从实时性,反应,效率来看,基于定时器的主逻辑都是最强的。 但是,万恶的但是来了,对于机器来说,完全基于tick来维护太难了。这需要能很细的拆解人物,分配到每个tick,然后还要为tick建立上下文,能保证持续性动作的正常实行。 这个难度太大了,很强,但性价比极地。 所以我完全不建议机器人的主逻辑以tick的方式来实现。 当然,比较简单又要求实时性的子系统可以基于tick处理,典型的就是战斗子系统。 触发入口 做一个特殊的触发,一般是利用Mud的回显机制,然后再执行玩任务是时候,想法让mud调用这个触发。 怎么说的,从一般客户端的设计,很容易的就会形成一套以出发为入口的代码,而且在初期很行之有效。 但是,如果要做一个足够复杂的机器,触发作为入口就会很勉强了,还要面对服务器本身的限制。 回调入口 回调作为入口,就纯靠代码来控制出发主循环。优点是不依赖mud,而且性能和可控性更高。 缺点就是需要有一个完整的统一的架构,将回调和回调后执行的代码组织起来,所有的代码也不能简单的通过出发执行,需要进行一定的抽象。 我的newhelljs就是利用回调入口的机制,执行完当前代码片段后,主动调用App.Next进行控制权的释放,由代码开始调用主逻辑。 主逻辑的组织 在决定主逻辑的调用方式后,我们就要决定主逻辑的代码怎么组织了。常见的可能有以下方法 触发多米洛骨牌 通过预先设置好的一系列触发,通过开关触发的形式,一步一步依次执行。 这种组织方式,配合合适的工具,在任务的小环节上极强的,我在newhelljs里也 做一个plan/task系统来更好的保证骨牌的稳定性。 但在宏观层面,整个机器人架构的层面,这种操作太细了,无法负担起过于复杂的机器架构 判断if队列。 就是收集可靠信息,然后通过一长串if(){}eleseif{} 的形式,判断当前应该做什么。 进一步的,可以采用责任链的实际模式,为不同的判断和对应的处理代码,封装为一个个的类,然后通过id进行全局注册。 这样可以通过一个id的数组,比如["hp","money","heal"]这样的形式,快速的定义和组合不同的工作流程了。 if队列在简单任务里效果极好。但复杂任务的话,复杂度就有点超过if队列的承受范围了。 在newhellljs里,我用责任链去实现了准备模块,效果非常好。 状态机模式 状态机模式是一个很有名的模式,但是在mud机器人里并不好用。 因为Mud机器人里的状态太多了,互相之间的转化太多,用状态机来处理mud,需要维护的状态会指数上升,是一个天文数字。 导致实际使用时都会进入一个中间态,而不是直接转换,很不适用。 我在pkuxkx.noob中尝试引入状态机,结果比较失败。 当然,状态机也有其优势,主要是可配置性强,可以显示的制定进入状态和离开状态时执行的代码。同时也的确客观存在状态这个存在。 所以我在newhelljs里还是小范围的引入了状态机。我给不同的任务设置了不同的stance,在进入不同组的stance会出发stance-leave和stance-enter,这样用户能很方便的在不同类型的任务(比如战斗/非战斗)任务之间执行一定的指令,这也是一个很明显的简化状态机模型。 指令队列缓冲模式。 指令队列缓冲模式指维护一个缓冲池,在空闲时依次弹出最前面的指令执行,然后缓冲池可以进行清除,插入,追加,压入新队列,快照/还原等操作。 这种模式,弹性很大,而且比较接近于人的思维模式,分配一系列任务,依次执行,遇到意外或者判断动态的切换之后的工作。 缺点一样,更复杂,而且更重要的是需要有完善的异常介入模式,对整个机器的完成度要求更高。 newhelljs中使用了Commands模块,把大部分功能都注册为Command,通过App.Next弹出新指令的方式来运行代码。 我的建议 我的建议是,主逻辑的入口,尽量用程序回调这种依赖最少的方式调用。 具体的组织,根据代码的规模 用责任链(固定流程)或者指令队列缓冲(动态维护流程)+意外处理的方式组织。
  • 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() } 这样就能确保每次在需要数据进行决策(准备)时,使用的是最新的有效的数据。
  • 0 赞同
    1 帖子
    24 浏览
    jarlyynJ
    单例模式,是设计模式的一种。它是一种创建型设计模式,它的核心目标是确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一的实例。 听着很牛吧?别管这个,这是被强OOP祸害的语言的鬼话。 单例模式本质就是,全局变量。 好了,使用lua的同学们放下手。全局变量不是你不用加local 来限制使用作用范围的意思。 单例可以通过全局变量来实现,但它强调的还是独一无二性,在英语里要加上the 定冠词那种(嗯,就是黑客帝国的the one)。 说明是一个特殊,独一无二的个体。 在代码开发中,很常见的一种模式是,将整个程序的功能,抽象成一个Applcation,然后建立一个全局单例 Applcation:Instance 来进行代码的开发。 在Javascript的管理,就是建立一个全局的app对象,Lua作为脚本语言,也可以适用这套模式。 那么,问题来了,我们为什么要用这个全局变量(单例)呢? 限制其他全局变量使用。 使用全局变量,就是能方便的在任何代码里进行数据分享/互相调用。 有需要就开一个全局变量,看似开发能快很多,但很快就会让代码失去可维护性,随便动一个变量都生怕整个程序崩了。 当我们有一个特殊的全局变量后,很自然的,我们需要调用的东西都会从这个全局变量(单例上找),不应该再开全局变量。 从这个角度来说,这个单例,就是我们整个机器人的the one 定义应用程序的根(root) 我看过很多全自动机器人代码。有一些,嗯,怎么说呢,让我感觉很乱,无从下手。 代码都是一个个平级的.lua/.js文件,互相之前会相互应用,要看很久才能推测出大概的功能划分。 按数据结构的说法,这些机器人的代码互相之间的关系是一个图(Graph),两两之间都可能有关联,很自由,很强大。 但对于我等普通人来说,难以驾驭。 相对而言,以我的能力,更能处理以数据结构的 树(tree) 的形式来组织的代码。 即从一个根room开始,不停分支,对应着一个一个的节点,和树杈一样。 我们在使用电脑时,最常见的树结构就是目录树,通过一个一个目录来对文件进行组织。 只有了一个固定的根,我们才能很容易的定义文件的代码的结构和组织结构。 给代码一个独一无二的挂载点。 上一个部分讲到了目录这个例子,那么我们扯远点说说Linux的文件管理。 linux下,讲究万物皆文件。不管你是设备,文件系统,文件,目录,都可以用mount的方式挂在到相对于根(root)目录的指定位置下。 那么,当我们使用单例App时,其实也是一样,把我们的代码/对外的结构,添加到单例和他的自元素上,就相当于做一个mount操作。 这时,每个挂载上去的代码,都成了一个单例(单例中的特定部分),都能很方便的找到。 在一般情况下,可能这个优势还不明显。 在javascript和lua中弱类型的脚本语言中,很难精确的定位到一个代码的位置。 那么,你觉得在你的代码中,是找到一个较 room的变量在哪里定义使用方便,还是一个App.Core.Map.Room 方便呢? 代码分组 还是文件目录的例子。 我们一般会怎么组织文件? 先把全放桌面的豪放流请下去。最常见的,其实是是按文件的用途或者特点,分为 文档,视频,照片,作业,学习资料什么的。 同样,既然我们把单例作为文件数来使用,我们也可以把代码和数据进行分区。 比如,主业务逻辑可以放在App.Core.XXX下,代码可以放在App.Data.XXX下,工具函数可以放在App.Utils.xxx下 这样在写代码时,可以一目了然,方便理解和组织。 总结 对于代码来说,全局变量(单例)是必不可少的,但是太多的单例会让代码复杂度指数上升。通过定义一个合适的根单例,我们能更好的组织代码,提可高维护性。
  • 深入浅出制作全自动Mud机器人-组件化

    Script脚本 全自动 机器人 架构
    1
    0 赞同
    1 帖子
    14 浏览
    jarlyynJ
    在全自动Mud机器人开发,以及长期的运维更新时,我觉得引入组件化是一件非常重要的底层架构。 组件化,看起来是一个和模块化接近的概念。但本质完全不同。 在我眼里,模块化是将相关的代码进行组织和封装,对外提供统一接口的一种代码组织形式。 组件化的确要依赖于模块化是实现功能。但更重要的是,组件化的目标是 可替代可废弃性。 随着Mud的更新,代码不应该进行大幅度的调整,还是废弃部分组件,新建部分组件,调整部分组件。这样,机器可以先将重点组件进行调整,然后临时禁用不重要的组件,甚至部分影响不大的组件带病工作,实现机器的边开发边运行。 既将代码根据易变程度分为不同的组织,确保核心代码的稳定性,然后组件中可以用各种快速/不健全/临时性的机制确保特殊状态下机器的可用性,以及确保机器处于一种可以从小到大,逐渐成长的状态。 为了实现这个目的,组件化需要实现的功能是 组件标准化 同一类的组件,应该有相同的接口和核心代码进行交互。这样,核心代码才能在调整主键时不需要关注细节和深入。 以我的newhelljs为例,使用了多种组件 角色准备阶段的提案组件 通过通用的Submit参数,实现判断是否进入某个准备阶段 任务本身的[Quest组件](https://github.com/hellclient-scripts/newhelljs/blob/main/script/src/core/quests.js 通过Ready参数,Cooldown属性,OnHUD/OnSummary|OnReport等接口,实现了人物与核心代码的隔离 配置的Condition组件 使得是否接任务/是否使用某个技能等等,都与核心代码无关,完全通过定义新的Condition实现。 很明显,组件标准化和能区分组件代码和核心代码的前提。如果不对组件进行标准化,则可扩展性可维护性也成了无根之水。 控制反转 控制反转(IOC)是一个常见的程序开发思路。 所谓的反转,就是将** 主程序创建类后,初始化类** 反转为 ** 主程序只负责创建类,类自行初始化) 实现了控制反转后,才能实现组件的封装,将组件与核心代码分离。 核心代码只负责两件事情: 如果你随手搜索一下,可能会发现一大堆依赖注入,工厂模式什么的复杂东西。 不用管那些,那些主要是对强OOP,静态编译的Java强调的东西。 对于js/lua的实现,参考我上面的例子中,都会传入一个函数作为参数就可以了。 把初始化函数放在组件的制定位置,核心代码创建组件后,把配置作为参数带入初始化函数就可以了。 这里说的控制反转,只是提醒一下,不要在核心端去设置组件,让组件自包含。 全局注册 全局注册很简单,就是建立一个全局的object/table变量,给每一个组件起一个唯一的字符串主键,将组件或者组件的生成函数都放在里面就行。 全局注册的目的是 在进行组件的替换/更新甚至顺序调整时,无需调整核心代码,只需要调整某个配置即可。 通过同一的调用方式,确保不会有其他位置的代码来直接调用组件内的代码,避免组件修改时出现全局问题。 方便的管理方式,能很容易的明白有多少个可用模块,不用去各个代码目录里寻找。 使用全局注册要注意的点是 只通过 注册的表 与组件交互,不建立直接处理的代码 在多个组件交互时,可以通过给组件加接口/全局事件/核心组里注册特殊毁掉的方式来处理。 重要的点 使用组件模式,其实是按照我们最初的目的来使用,也就是 将代码分为干净的核心代码,和脏(与业务更接近)的组件代码。确保代码指令控制在一个基本的可用度上。 通过配置,设置,能动态的调整激活的模块,实现更复杂的功能。 技术只是手段,代码如何处理,不一定要强求某个模式,还是为了实际目的服务。
  • 我使用的一种机器人代码分层组织方式

    Script脚本 机器人 架构
    1
    0 赞同
    1 帖子
    15 浏览
    jarlyynJ
    在写gui程序时,我整理了一套代码组织方式 参看 HellMapManager项目就是按这个架构来组织的。 对于Mud机器人,我觉得这个组织形式也适用。当然,由于主要业务不同,住址形式也会不同。我只是将所有的代码根据对应的层次打个标签,避免不同层级的代码过于混淆。 UI 交互层 这个完全绑定于客户端的实现,在Mud机器人里也相对不重要,可以和Service层混在一起 Service 服务器层。从字面意义上来说,就是为最终用户提供的功能的封装层。UI交互的内容最终绑定到服务层上。最大的用途是防止用户UI直接操作到业务层,做封装和拦截,以及抽象。 Core 核心(业务) 层。对于Mud机器人来说,绝大部分的代码都是核心层。所以实际写机器人的时候Core肯定还要做细分,比如底层核心和任务模块。 Helper 辅助类,业务层和数据层直接的纽带,将数据的细节对业务做一定的封闭。 Adapters 适配器层 抽象底层交互。对于Mud机器人来说,就是将客户端的触发/别名/计时器做一个抽象,以及对应的事件Event框架。引入这个层的话能提升机器人可迁移性和做测试的可能。 Model 模型 在各个业务层中共同的数据结构。在Mud中比如房间,玩家,道具等等。 utils 工具层。比如中文转数字,格式化文字等纯与业务无关的,全局都可能使用的代码
  • 深入浅出制作全自动Mud机器人-运维通知

    Script脚本 全自动 机器人
    1
    0 赞同
    1 帖子
    13 浏览
    jarlyynJ
    在引言中,我们提到过,制作全自动机器人的一个重要课题是 怎么才能让循环更稳定,控制更多的资源和状态? 那么,提升稳定性的一个重要的工作,就是当服务(机器人)发生意外时,能及时的通知运维(开发者/用户)查看问题,进行操作。 就是当机器停顿,无法继续,甚至ID死亡时,以方便的手段通知用户,记录问题现场,恢复程序执行。 这也是一个标准的运维工作。 所以我建议参考现在主流的运维工作流程。 邮件形式 Hellclient最早是支持嵌入STMP信息,提供了内建方法可以发送电子邮件。 但在实际使用时,基本处于被废弃状态。 因为现在主流的运维通知终端是手机,安卓或者iOS。 邮件通知在手机上面临了一下问题 手机设备上,邮件客户端无法保证后台常驻,实时性往往有很大问题。 邮件客户端又有各种实现,对于链接的处理形式不同,无法保证唤起我的APP端。 邮件的安全级别很高,所有各个邮件服务器的安全设置很复杂,对用户并不友好。 通知服务 我的Hellclient UI项目曾经实通过腾讯推送实现过原生的推送功能。 但现在这个功能已经被废弃了。 通知服务会涉及到几个难点 用户识别问题。安卓客户端还能自行申请推送服务,iOS不现实,发布时就绑定应用ID。 保活问题。对于国内环境,国产安卓是绕不过去的槛。各种手机的保护和续航是很麻烦的点,只是做一个mud相关的app没法投入这么多精力去处理 这写严重的问题随着新的方案出现,对于Hellclient UI项目属于验证的得不偿失,所以废弃了这个功能。 Webhook 目前的运维主流的通知手段其实还是webhook的形式。 就是在各种工作流软件中,会提供一个标准的,地址带身份验证token的http接口。 主要对这个接口发送特殊格式的内容,就会通过服务器向对应的手机APP发送一条通知。 处于这个通知的专用型,一般都能确保拉起手机中注册过的APP直接处理。 可以说完美的适合了Mud机器人,作为一个标准意义上的服务的运维要求。 而且webhook形式的要求最低,基本不需要客户端做特殊支持,只要提供标准的HTTP接口和JSON支持就可以了。 国内比较常见的Webhook供应商为 企业微信 钉钉 飞书 国外知名的有 Slack 根据我自己的使用体验和经验,我的机器人使用的飞书通知。目前使用体验良好。 如果不喜欢单独安装App的话,也可以考虑使用企业微信。企业微信可以绑定到微信内,我个人有其他业务是使用企业微信通知的。
  • 深入浅出制作全自动Mud机器人-目录

    已固定 Script脚本 全自动 机器人
    1
    0 赞同
    1 帖子
    57 浏览
    jarlyynJ
    序 引言 架构 全局单例 组件化 事件总线 选项模式 核心 主逻辑 数模转换 数据与缓存 准备阶段 指令队列 用户队列 任务编排 战斗子模块 战斗系统 移动子模块 移动模块 移动确认 移动失败处理 惯性导航 移动模块拆解 路线规划配置 迷宫 遍历 组件 预期管理 同步 对象列表 配置 条件指令 周边 运维通知
  • 深入浅出制作全自动Mud机器人-引言

    Script脚本 全自动 机器人
    1
    0 赞同
    1 帖子
    25 浏览
    jarlyynJ
    全自动Mud机器人是一种很典型的完美程序 完美的功能,解放人力 高度稳定性的代表 全面处理随机和意外 可以说,是对编写代码有兴趣,有热爱的Dream Project。 很多Mud玩家成为“高手”的标志之一。 当然,也会让很多人觉得,这很难,我做不到。 其实,只要开始做,全自动Mud机器人并没有这么难。 本系列的文章,会涉及编写全自动机器人的方方面面,帮助读者写出一个属于自己的,能顺畅运行的全自动机器人。 当然,在开始前,我们先要解决一个问题: 什么是全自动机器人? 故名思议,全自动机器人就是能不需要人照顾的,自动完成某个人物的机器人。 理论上说,用一个定时器,每隔半小时在公开频道发布你要出售的道具的机器人,也算全自动机器人。 但这明显不符合我们的目标。 在此,我给全自动机器人下一个定义,全自动机器人是符合这样的两个条件而代码 在不需要人力维护的情况下,形成一种稳定循环,能控制状态和各种资源正向发展,不至于陷入困境而被动停摆 能够完成复杂任务获得回报 在这个定义下,我们能够明确的描述出我们这个系列研究的课题: 怎么才能让循环更稳定,控制更多的资源和状态? 怎么才能完成更复杂的任务,获得更好的回报?