跳转至内容
  • 深入浅出制作全自动Mud机器人-选项模式

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