跳转至内容
  • 0 赞同
    1 帖子
    14 浏览
    jarlyynJ
    对于整个移动模块来说,最核心的可能就是移动失败处理了。 本身对于大部分系统来说,失败/意外流程处理都是最复杂的。而移动模块的失败处理大部分面对的是wiz的恶意,所以更强调可扩展性和可维护性。 对于移动失败,本质来说,就是: 发出移动指令 2.预期会收到成功信号,或者各种失败信号 3.如果收到成功信号,则对当前移动的这一步进行核销。 4.如果受到的是失败信号,则根据具体的失败信号数据进行处理,重规划路线/重试/放弃失败。 因此,移动失败是一个很典型是预期管理 我做的预期管理模块有很大一部分目的就是为了移动失败处理设计的。如果是其他语言/客户端的机器,也能通过各种类库或者触发器分组的功能进行预期管理。 以newhelljs为例,代码地址 App.Map.StepPlan = new App.Plan( App.Map.Position, function (task) { App.Move.RetryStep = false let tt = task.AddTimer(App.Map.StepTimeout, function (timer) { return App.Map.OnStepTimeout() }).WithName("timeout") task.AddCatcher("core.longtimestep", function () { tt.Reset(App.Move.LongtimeStepDelay) return true }) task.AddCatcher("core.retrymove", function () { App.Move.RetryStep = true return true }) task.AddCatcher("core.movereset").WithName("movereset") task.AddCatcher("core.wrongway").WithName("wrongway") task.AddCatcher("core.walkbusy").WithName("walkbusy") task.AddCatcher("core.walkresend").WithName("walkresend") task.AddCatcher("core.walkretry").WithName("walkretry") task.AddCatcher("core.walkfail").WithName("walkfail") task.AddCatcher("core.blocked2", (catcher, event) => { if (App.Core.Room.Current.ID == "") { return true; } }).WithName("blocked2") task.AddCatcher("core.blocked", (catcher, event) => { catcher.WithData(event.Data) }).WithName("blocked") task.AddCatcher("core.needrest").WithName("needrest") }, function (result) { switch (result.Type) { case "cancel": break default: switch (result.Name) { case "timeout": break case "movereset": App.Map.Room.ID = "" App.Map.Retry() break case "wrongway": if (App.Move.RetryStep) { App.Map.Resend(0) return } App.Map.Room.ID = "" App.Sync(() => { App.Map.Retry() }) break case "walkbusy": App.Map.Resend() break case "walkresend": App.Map.Resend(0) break case "walkretry": App.Map.Retry() break case "blocked": App.Move.OnBlocker(result.Data) break case "blocked2": App.Core.Blocker.BlockStepRetry() break case "needrest": App.Move.NeedRest() break case "walkfail": App.Move.OnWalkFail() break default: } } } ) 很明显,是对于整个移动做了预期。 首先这个预期是基于App.Map.Position的 Map库中有两个Position App.Map.Position 当前房间范围 App.Map.MovePosition 当前移动的范围 所以,当成功移动时,整个移动处理会因为离开范围而被"cancel" 当没有Cacnel时,说明意外出现了。 不同的信号(事件)会根据在switch里做分支判断,调用各种异常。 这也就是整个失败处理的底层逻辑 在 完全基于hmm开发的hongchengjs中,我为了可维护性,做了些调整,将常见的移动失败作为一个文本保存,启动时调入内存放在一个Hash表里,直接做表匹配模拟事件,算是一个小的结构优化。 代码地址 在失败处理中,有四个处理是最为典型的 第一个是路线错误 也就是"core.wrongway"事件。 触发后会清空当前房间信息,调用当前移动的Retry方法进行重新规划和移动。 第二中是路线禁用 也就是"core.blocked2"事件。 出发该事件后,说明被拦截,而且不该击杀然路Npc,会把当前房间和目标房间的出口临时禁用,然后重新规划路线。 第三种是插入战斗 "core.blocked"事件 会把当前移动进行快照,然后击杀拦路npc,再对快照进行还原,继续移动。也是很经典的形式。 最后是重新规划 "walkretry"事件 这个事件和路线错误一样都是直接调用重新规划。 区别是这个事件本质是一个衍生事件,在我的事件系统中,一般会调用Filter进行处理,一般是调整当前Move的上下文标签。然后再根据最新上下文标签进行重新规划。 这四种基本就是主要的移动失败处理的内容。当然,不同的Mud可能还会有一些细节上的调整。 移动失败处理的主要处理方式就是 重试。 调整上下文,重新规划,避免失败出口。 挂起移动,解决意外,还原移动并继续,
  • 深入浅出制作全自动Mud机器人-选项模式

    已移动 Script脚本 mud机器人 全自动 架构
    1
    0 赞同
    1 帖子
    18 浏览
    jarlyynJ
    选项(Option)模式是一种新近流行的设计模式,用于配置类,主要流行于于Go语言(golang)。 每个设计模式本质上就是正对一个场景的一个常见小招式。 选项模式也不例外。它解决的主要问题是: 复杂系统的可复用编码配置。 典型的选项模式的形式是 var system=Systm.New(opt1,opt2,opt3...) 通过opt1,opt2,opt3来对系统进行初始化。 复杂系统 选项模式正常情况下一定是解决复杂系统的问题的。 甚至可以简单粗暴的认为选项模式是一个改良的构建者(builder)模式,即配置器列表。 不复杂的系统没有使用选项模式的必要。 对于选项模式,最典型的就是和Builder模式一样,用来处理一个封装类,类里封装了大量的函数/接口,可以根据需要去把这些函数进行替换。 默认可用及扩展性 选项模式的优势其实很大一部分体现在可扩展性上。 由于选项模式的选项都是可选的,而且不可能在代码写完后自动扩展。 所以,选项模式中可以认为一定有部分选项没有被配置过。新建的封装类一定在所有选项中可以运行的默认值。 同样,因为有这个默契,大部分采用了选项模式架构的代码一定有很好的兼容行。因为添加的新的功能和实现必须与原有的行为表现一致。 可以认为这是对代码结构的一种架构上的约束。 基于功能/模块配置 选项模式的主要价值,就是体现在可以分块的,可选的进行配置。 因此,从逻辑上来说,每个选项代表了一种功能,或者模块。 具体来说,举个自理,我的go语言http请求库,默认情况下会生成一个标准请求。比如: var myreq=preset.New(Host("http://www.baidu.com/) 如果我这是一个Post请求,需要带一个纯文字body,代码就变成了 var myreq=preset.New(preset.Host("http://www.baidu.com/,preset.POST,preset.StringBody("12345")) 而这个请求如果还需要带上一个特定的请求头,那么就是 var mreq=preset.New(preset.Host("http://www.baidu.com/,preset.POST,preset.StringBody("12345"),preset.Header("auth","abc")) 从这个例子应该能喊好的看出选项模式的主要用法了。 在构建复杂系统时,按功能和模块来进行配置,同时提供很多预制件,可以直接初始化为需要的功能。 在系统进行升级扩展时,由于保持默认行为的一致性,也会有很好的兼容性。 在Mud机器人中的应用 从选项模式的特点来看 系统复杂 高扩展性 功能化,模块化 也只有移动模块这个复杂系统需要用得上选项模式。 我在newhelljs的移动模块就是使用了选项模式的架构,具体可以看相关的内容。
  • 深入浅出制作全自动Mud机器人-数模转换

    Script脚本 mud机器人 全自动 架构
    1
    0 赞同
    1 帖子
    16 浏览
    jarlyynJ
    数模转换其实是一个很古早的概念了。 就是将原始的模拟信号,转换成数字信号,然后数字化的系统才能根据数字信号进行操作。 首先,从严格意义上说,服务器返回的文字信息本质是ansi控制文本,是一种带显示控制的控制,目的是进行一种美观的带格式的外观描述,这的确是一种 数字化程度很高的 模拟信号。 其次,对于实际的复杂业务来说,把数据抽象成业务数据,将代码隔离为业务层和交互层也是很通用的一种处理技巧,在实际需求上,的确有分割交互层(模拟信号)和业务层(数字信号)的意义。 所以,进行数模转换是一种我很推荐的架构形式。 数据标准化 数模转换而第一步是信息标准化。 也就是,将所有的信号,转为一种有约定格式和共性的基础信号。 很多时候,处理程序还需要对信号做一个2次处理,抽取和判断后生成原生信号的衍生信号。 在这个背景下,处理业务数据的代码才能做到信号形式的无关性。 具体到代码的例子。我在newhelljs里引入了 App.LineEvent = function (name) { return App.Engine.LineEvent(name) } App.FilterLineEvent = function (filtername, eventname) { return App.Engine.FilterLineEvent(filtername, eventname) } 两个方法。 App.LineEvent是直接将一个mud行的出发,抛出为一个制定name的event App.FilterLineEvent是定义一个处理器,在处理器里做预处理和判断,然后确定没问题的话再以eventname抛出事件 App.FilterLineEvent的应用范例: //处理房间名 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) } }) 接到可能是房间名的信号后,判断是否符合条件,符合条件再抛出事件。 信号数据采样 在mud中,除了根据行进行分析的时间外,还有很多直接的数据信号取的护理。比如hp,score,skills,i等指令。 这时候需要将传输来的模拟信号(一般是固定格式的表格列表)转换为合适的数据模型,再加以保存。 对于能够主动触发的数据,也要做好缓存的处理。 工作重点 对于根据数模转换对代码进行分层,我们要明白主要的工作重点,这样能更好的根据实际情况进行合理规划。 我们的数模转换的核心目的是 让代码更清晰,易于维护,代码出现问题时能快速定位到相应模块 将业务与实际表现剥离,这样在Mud进行更新/更改界面时,不会需要对业务层进行调整。比较表现层比业务层测试快的多。
  • 深入浅出制作全自动Mud机器人-遍历

    Script脚本 mud机器人 全自动
    1
    0 赞同
    1 帖子
    11 浏览
    jarlyynJ
    在Mud机器人中遍历其实有两种含义。 第一,依次经过所有给到的房间。 第二,依次经过给到的路径,在需要停止时暂停。 这里主要讨论第二种。 之前说过,每个移动是一堆和移动相关的判断函数的集合。 这里就是要实现继续移动或中止的判断。 参考我的core/zone.js,遍历而实现代码是 App.Zone.Finder = function (move, map) { wanted = App.Zone.Wanted move.Option.MultipleStep = wanted.SingleStep != true move.OnRoom = function (move, map, step) { let item = wanted.Checker(wanted) if (item) { wanted.Name = item.GetData().Name wanted.ID = item.IDLower if (map.Room.ID) { wanted.Loc = App.Map.Room.ID Note(wanted.Target + " @ " + wanted.Loc) } } } move.OnArrive = function (move, map) { if (wanted.Loc) { App.Map.FinishMove() return } wanted.Next(map, move, wanted) } } 很明显,这个Finder是用App.Zone.Wanted作为选项,对move进行了初始化。 这里覆盖了两个方法 move.OnRoom 经过房间(房间信息结束)的触发 move.OnArrive 移动结束的触发。 主要是因为可以多步移动。 代码使用 App.Zone.Wanted.Checker来判断房间中是否有遍历目标。 如果找到目标,则遍历结束(App.Map.FinishMove)。 是一个很标准的任务遍历模型。
  • 0 赞同
    1 帖子
    8 浏览
    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
    0 赞同
    1 帖子
    6 浏览
    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) 这个超时类,以便定位时被拦能超时重试。
  • 0 赞同
    1 帖子
    7 浏览
    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
    0 赞同
    1 帖子
    6 浏览
    jarlyynJ
    所谓的惯性导航,其实就是在机器人进行移动时,能推断出自己目前的位置。这样在移动中和移动结束时不会因为丢失位置信息而需要重新定位。 实现惯性导航的方式我见过不少。有根据地图计算下一步的房间的,甚至有监听发送的指令来进行计算的。 从我的理解来看,惯性导航,和真实的导航APP一样,代表一种预期。就是我走到这一步后,我应该在什么位置。 这是在路径计算时,已经计算好的预期。 如果行进中发生了意外,比如路线错误/被随机移动/被挡路,则应该将条件设置好后重新计算路线。 就如同导航APP的"已经选择新路线,新路线快/慢XX分钟"那样。 所以,在路线规划时,不管是移动,还是遍历,不光光应该获取到指令列表,还要获取到每个指令对应的目标,这样才能更好的进行位置计算。 当然,基于预计算的惯性导航也容易出问题,典型的就是位置计算错误后,会卡在某地。 但个人认为,显性的Bug比隐性的Bug好,容易复现的Bug会更容易解决。
  • 深入浅出制作全自动Mud机器人-移动系统

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