跳转至内容
  • 1 帖子
    87 浏览
    jarlyynJ
    对象(Object)是Mud底层 的Mudos/Fluffos的核心概念。 每个房间/npc/道具都是一个Object,每个Object 内能包含一系列其他Object的列表。 所以,作为一个针对运行在Mudos/Fluffos上的机器人,必然需要面对大量的Object的处理。 我在newhelljs中创建了一个Object类来专门处理这个问题 代码地址 代码中对应了两种概念 Object对象 Object对象中包括了 Label(显示的名字),ID(物品ID),IDLower(小写化的ID),惰性加载的 数量,单位,名字,依据可选的Param 对应的就是Mud中最基本的Object概念。 不论房间中的对象,还是身上的道具,都抽象成这样的一个标准类。 List对象类表 对象列表是一个非常实用的类。 它除了用来放置所有的Object对象外,还提供了一些快速的查找过滤的方法 FindByID 通过ID(大小写敏感)查找 FindByIDLower 通过ID(大小写不敏感)查找 FindByKey 通过特意制定的Key查找 FindByLabel 通过显示的标签(带数量)查找 FindByName 通过解析后的不带数量的名字查找 SearchByName 部分匹配不带数量名字查找 SearchByLabel 部分匹配带数量名字查找 SearchByID 部分匹配id查找 ExcludeID 排除ID查找 FindByFilter 自定义过滤函数查找 以及一些快速方法 First 返回列表中的一个元素 Last 返回列表中的最后一个元素 Sum 统计列表中的数量总和 来覆盖了绝大部分Mud任务的需求。 毕竟本质上,由于Mud底层的实现,除了解密类任务,大部分任务主要就是针对道具,以及房间中的 NPC进行交互。 只要能及时把身上的道具和房间里的NPC抽象为Object和List,做对应的任务可以说易如反掌。 道具处理代码参考 let matcheritem = /^( |□|○)(.+)\(([^\(\)]+)\)$/ let matcherend = /^目前携带了(.+)件物品。$/ //道具统计的计划 let PlanOnItem = new App.Plan( App.Positions.Connect, function (task) { let data = {} task.AddTrigger(matcheritem, function (task, result, event) { let item = new objectModule.Object(result[2], result[3], App.History.CurrentOutput) let index = data[item.IDLower] if (index == null) { index = 1 } switch (result[1]) { case "□": item.Mode = 1 break case "○": item.Mode = 2 break default: item.Mode = 0 } item.WithKey(item.IDLower + " " + index) App.Data.Item.List.Append(item) data[item.IDLower] = index + 1 return true }) task.AddTrigger(matcherend, function (task, result, event) { App.Data.Item.Count = objectModule.CNumber.ParseNumber(result[1]) }) }, function (result) { checkerI.Reset() }, ) ... 房间NPC处理代码参考 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") }) ...
  • 深入浅出制作全自动Mud机器人-路线规划配置

    Script脚本 mud机器人 全自动
    1
    1 帖子
    82 浏览
    jarlyynJ
    之前有提过,我的最新的机器人的路线规划都是通过HellMapManager和hellmapmanager.ts来实现的。 在HMM中规划路线有3个方法 APIQueryPathAny 点对点规划,对应To APIQueryPathAll 多点遍历,对应Rooms APIQueryPathOrdered 顺序遍历,对应Ordered 在外加一个无视地图信息的Path,就是我的地图模块的主要路径规划方式了。 很明显,为了性能考虑,4种路线都是预先生成所有路径的。如果对应到导航App,就是一个大大通向的目标的绿线已经画好了。 但在实际使用时,我并不会把所有路径暴露给外部。 实际上,To,Rooms,Ordered,Path显示的实现了2个方法 Next(move, map) 返回下一步移动的指令 Retry(move, map) 重试(重新规划) 正是为了实现重新规划(其实还有迷宫的因素),所以路线规划时是一次全部生成,再分小步喂给行走模块,按需进行重新规划的。 这也和主流的导航 APP的操作一致。 当然,由于To,Rooms,Ordered是和当前位置相关的。 为了实现重新GPS定位,调用了内建的Locate类,因此还需要接管 OnStepTimeout(move, map) 这个超时类,以便定位时被拦能超时重试。
  • 1 帖子
    80 浏览
    jarlyynJ
    在移动模块介绍时我们说过,整个移动模块是Mud机器中最复杂的模块。 同时,移动模块又是Mud机器人中最底层最常用的模块,所以,怎么合适的设计移动模块是整个全自动机器人的核心问题之一。 我在不同时代的机器中使用了不同的架构模式(helllua,pkuxkx.noob,newhelljs) 最后采用的是 一个基于组合模式和选项模式的架构。 将移动的需求拆解为不同的处理函数,用默认值组合成一个标准移动 通过同一的选项模式进行设置,这样可以通过预设置的配置,迅速组建成一个可用的复杂移动 拆解 首先,我将所有移动统一,拆解为基础功能,以及关键点的处理函数。 把所有的处理函数综合起来,就是一个移动 范例代码 class Move { StartCommand = "" Data = {} Retry = DefaultMoveRetry Next = DefaultMoveNext OnRoom = DefaultMoveOnRoom OnArrive = DefaultMoveOnArrive Vehicle = DefaultVehicle OnFinish = module.DefaultOnFinish OnCancel = module.DefaultOnCancel OnInitTags = DefaultOnInitTags OnStepTimeout = DefaultOnStepTimeout OnStepFinsih = DefaultMoveOnStepFinish MapperOptionCreator = DefaultMapperOptionCreator Option = new Option() ... } 非常明显,我将所有的方法的默认方法,汇总成了一个 Move类。这样只要替换对应的方法,就能实现不同的移动了。 整个 Move类相当于一个乐高积木,通过不同的积木进行拼装,然后我的Map类有一个StartMove方法 StartMove(move) { this.Move = move this.MovePosition.StartNewTerm() this.ChangeMode("") move.Walk(this) } 将拼装好的Move类传入,就能开始移动了 配置 在配置上,我采用了Go语言中十分流行的选项(Option)设计模式 也就是,通过一个选项列表对Move进行设置,每个选项代表一套预定义的函数包,应用完后就是我们需要的Move对象。 对于选项列表,我创建一个路书(Route)对象来保存。 这样,所有的移动,就是一套预设的路书,初始化为移动,交由Map对象执行。 我预设的移动选项包括 To 定点移动 Path 固定路线遍历 Ordered 顺序遍历房间 Rooms 房间遍历 SingleStep 单步移动 StartCommand 开始指令 Option 地图选项 Tag 移动标签 这些就能决定移动的 类型/形态 然后在消费的模块,加入目的型的选项 比如core/zone.js的 App.Zone.SearchRooms let move = wanted.Ordered ? App.Move.NewOrderedCommand(rooms, App.Zone.Finder) : App.Move.NewRoomsCommand(rooms, App.Zone.Finder) 就很简单的拼装出了一个便利搜索的移动。 通过 基础模块(生产端)的 移动形态/类型 选项,和任务(消费端),快速的组合出需要的移动模块。
  • 深入浅出制作全自动Mud机器人-惯性导航

    Script脚本 mud机器人 全自动
    1
    1 帖子
    87 浏览
    jarlyynJ
    所谓的惯性导航,其实就是在机器人进行移动时,能推断出自己目前的位置。这样在移动中和移动结束时不会因为丢失位置信息而需要重新定位。 实现惯性导航的方式我见过不少。有根据地图计算下一步的房间的,甚至有监听发送的指令来进行计算的。 从我的理解来看,惯性导航,和真实的导航APP一样,代表一种预期。就是我走到这一步后,我应该在什么位置。 这是在路径计算时,已经计算好的预期。 如果行进中发生了意外,比如路线错误/被随机移动/被挡路,则应该将条件设置好后重新计算路线。 就如同导航APP的"已经选择新路线,新路线快/慢XX分钟"那样。 所以,在路线规划时,不管是移动,还是遍历,不光光应该获取到指令列表,还要获取到每个指令对应的目标,这样才能更好的进行位置计算。 当然,基于预计算的惯性导航也容易出问题,典型的就是位置计算错误后,会卡在某地。 但个人认为,显性的Bug比隐性的Bug好,容易复现的Bug会更容易解决。
  • 1 帖子
    99 浏览
    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处理函数进行处理,处理完毕后抛出事件通知需要后续操作的模块。 这样才能最大的程度的解耦合,并保证房间处理信息的内聚。 不做切割的话,很容易随着房间信息格式的调整,把整个代码搅乱。
  • hellmapmanager.ts项目介绍

    已固定 HellMapManager地图编辑器
    1
    1 帖子
    145 浏览
    jarlyynJ
    hellmapamanager.ts是一款将HellMapManager中C#的数据维护/调用代码用Typescript重写的项目。 用于在不使用http接口的情况下,使用v8/luajit等高性能脚本引擎直接调用HellMapmanager中的相应算法。 项目支持 hmm格式文件的维护 编译到javascript/lua格式 项目地址为 https://github.com/hellclient-scripts/hellmapmanager.ts
  • HellMapManager软件介绍

    已固定 HellMapManager地图编辑器
    1
    1 帖子
    106 浏览
    jarlyynJ
    HellMapManager是一款使用c#语言开发,支持 Windows/Linux/MacOS的地图编辑/路径规划软件。 软件支持 hmm/hmz格式地图信息 地图信息维护 地图版本对比 补丁生成和选择性应用 通过http接口方式进行调用 无头模式 等功能 项目地址为 https://github.com/hellclient-scripts/hellmapmanager
  • 深入浅出制作全自动Mud机器人-移动系统

    Script脚本 mud机器人 全自动
    1
    1 帖子
    110 浏览
    jarlyynJ
    在mud系统中,地图系统是最复杂的一个系统(相对于任务系统,心跳/属性系统,战斗系统)。 而且一般的中文Mud系统有没有普遍的Busy机制,部分Mud的wiz对添加地图也没有克制力,使得地图整个地图系统往往有超过手动玩家能力的复杂性。 所以移动系统也是整个Mud机器人中最重要和复杂的系统。 在这里我们也会分很多的章节去描述怎么做好Mud机器人里的移动系统。 当然,我们先要明白我们需要一个怎么样的移动系统 伪动态规划的移动系统 由于Mud地图的特性,在每个出口上经常有各种条件和限制。 地图的任务和NPC也经常有区域性的限制,所以我们往往需要有一个伪动态规划的地图系统。 为此我特地写了HellMapManager和hellmapmanger.ts 两个项目来更好的实现这点。 关于伪动态规划的内容参见这个链接 兼容迷宫的移动系统 一般我们地图规划其实是基于 一串固定指令对应一次移动的。 但很多时候我们需要引入迷宫系统,就是需要判断,处理,多次移动来构成一次虚拟移动的情况。 我称之为迷宫系统。 怎么兼容迷宫系统,怎么把移动移动和普通移动统一起来,这也是一个重要的课题。 可重计算重试的移动系统。 对于Mud机器人来说,一次移动其实也是一次预期。 预期的路线,和实际能行舟的路线可能是有区别的。 所以我们的移动系统应该是可重试,对不可进入的房间能屏蔽,能动态调整移动参数后继续尝试的系统。
  • 深入浅出制作全自动Mud机器人-同步

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

    Script脚本 全自动 机器人
    1
    1 帖子
    85 浏览
    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稳定的基础其实就是准备阶段。准备阶段大部分情况下是整个机器最复杂的部分,机器架构的很重要的一部分就是把复杂性统一放在准备阶段,这样任务部分就很轻很简单了。 同样,准备是一个很好的测试机器人稳定性的工具。 我们可以做一个空循环 信息采集->准备阶段->等一秒 空跑一段时间,就能验证整个机器人底层框架的稳定性,健壮性,以及是否有内存泄漏之类的问题了。
  • HellclientUI 2026春季版发布

    HellclientUI应用
    1
    1 帖子
    165 浏览
    jarlyynJ
    更新内容: 修正连接按钮状态bug 部分UI调整 iOS及MacOS版请在AppStore更新 发布地址
  • 深入浅出制作全自动Mud机器人-主逻辑

    Script脚本 全自动 机器人 架构
    1
    1 帖子
    83 浏览
    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弹出新指令的方式来运行代码。 我的建议 我的建议是,主逻辑的入口,尽量用程序回调这种依赖最少的方式调用。 具体的组织,根据代码的规模 用责任链(固定流程)或者指令队列缓冲(动态维护流程)+意外处理的方式组织。
  • HellMapManagerGUI界面数据过滤介绍

    HellMapManager地图编辑器
    1
    1 帖子
    171 浏览
    jarlyynJ
    HellMapManager的数据列表视图都会提供筛选功能。 最基本的功能就是输入一个关键字,比如一个Key,那么,所有主要属性包含这个Key的对象都会被过滤出来。 但有时候,我们希望有精确匹配,这时候我们会需要掌握一些特殊的语法 多关键字 用逗号分隔多个关键字,可以做多种筛选 比如搜索 扬州,客房 能过滤搜索所有信息中有扬州和客房的信息 注意,分割后的关键字,前置和后置的空格都会忽略 精准匹配 比如我们要搜索所有和abc匹配,但不包含abcd,0abc等信息的,可以用前置的等号进行匹配 =abc 这样,只会精准的把和abc有关的匹配出来 属性匹配 有一个常见需求,就是过滤房间key为abc的 key=abc 或者有出口到abc的 to=abc 可以在等号前加入类型。 目前支持的类型为 key 主键 name 名字 group 分组 type 类型 desc 描述 message 信息 to 目标,房间列表中包含 command 出口指令 tag 标签或者环境条件/房间条件 misc 杂项(目前是房间数据的Key和Value) 取反 关键字最前方加入英文感叹号!,就能取反,即过滤不符合条件的 !key=abc 特别的,tag和misc很特别,只要有任何一个tag/key不符合即可,基本不会使用取反。 转义 为了输入特殊字符,过滤字段支持转义 转义前 转义后 \\ \ \空格 空格 \, , \= = \! ! \n 换行