<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Script脚本]]></title><description><![CDATA[一起来沟通Mud机器人脚本]]></description><link>https://forum.hellclient.com/category/8</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 09:09:13 GMT</lastBuildDate><atom:link href="https://forum.hellclient.com/category/8.rss" rel="self" type="application/rss+xml"/><pubDate>Fri, 06 Feb 2026 07:54:23 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[深入浅出制作全自动Mud机器人-移动失败处理]]></title><description><![CDATA[对于整个移动模块来说，最核心的可能就是移动失败处理了。
本身对于大部分系统来说，失败/意外流程处理都是最复杂的。而移动模块的失败处理大部分面对的是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) =&gt; {
                if (App.Core.Room.Current.ID == "") {
                    return true;
                }
            }).WithName("blocked2")
            task.AddCatcher("core.blocked", (catcher, event) =&gt; {
                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(() =&gt; {
                                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可能还会有一些细节上的调整。
移动失败处理的主要处理方式就是

重试。
调整上下文，重新规划，避免失败出口。
挂起移动，解决意外，还原移动并继续，

]]></description><link>https://forum.hellclient.com/topic/39/深入浅出制作全自动mud机器人-移动失败处理</link><guid isPermaLink="true">https://forum.hellclient.com/topic/39/深入浅出制作全自动mud机器人-移动失败处理</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Fri, 06 Feb 2026 07:54:23 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-选项模式]]></title><description><![CDATA[选项(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的移动模块就是使用了选项模式的架构，具体可以看相关的内容。
]]></description><link>https://forum.hellclient.com/topic/38/深入浅出制作全自动mud机器人-选项模式</link><guid isPermaLink="true">https://forum.hellclient.com/topic/38/深入浅出制作全自动mud机器人-选项模式</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Thu, 05 Feb 2026 07:03:01 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-数模转换]]></title><description><![CDATA[数模转换其实是一个很古早的概念了。
就是将原始的模拟信号，转换成数字信号，然后数字化的系统才能根据数字信号进行操作。
首先，从严格意义上说，服务器返回的文字信息本质是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 &amp;&amp; words[0].Color != "" &amp;&amp; words[0].Bold == true) {
            App.RaiseEvent(event)
        }
    })

接到可能是房间名的信号后，判断是否符合条件，符合条件再抛出事件。
信号数据采样
在mud中，除了根据行进行分析的时间外，还有很多直接的数据信号取的护理。比如hp,score,skills,i等指令。
这时候需要将传输来的模拟信号(一般是固定格式的表格列表)转换为合适的数据模型，再加以保存。
对于能够主动触发的数据，也要做好缓存的处理。
工作重点
对于根据数模转换对代码进行分层，我们要明白主要的工作重点，这样能更好的根据实际情况进行合理规划。
我们的数模转换的核心目的是

让代码更清晰，易于维护，代码出现问题时能快速定位到相应模块
将业务与实际表现剥离，这样在Mud进行更新/更改界面时，不会需要对业务层进行调整。比较表现层比业务层测试快的多。

]]></description><link>https://forum.hellclient.com/topic/37/深入浅出制作全自动mud机器人-数模转换</link><guid isPermaLink="true">https://forum.hellclient.com/topic/37/深入浅出制作全自动mud机器人-数模转换</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Thu, 05 Feb 2026 05:32:04 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-遍历]]></title><description><![CDATA[在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)。
是一个很标准的任务遍历模型。
]]></description><link>https://forum.hellclient.com/topic/36/深入浅出制作全自动mud机器人-遍历</link><guid isPermaLink="true">https://forum.hellclient.com/topic/36/深入浅出制作全自动mud机器人-遍历</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 03 Feb 2026 15:21:49 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-迷宫]]></title><description><![CDATA[mud中，迷宫是一个很重要的课题。
其实这里说的迷宫，并不仅仅指迷宫这个解密形态，指的是非标准移动，动态出口。
有些需要特殊物品/ask npc才能通过的出口，其实也是迷宫的一种。
在我的第三版移动代码里
代码地址
Maze实现的依然有点丑。如果我还有第四版的移动代码的话，应该就是重写这一块了。
每个Maze类需要实现实现以下方法

CheckEnter 检查是否进入了该迷宫
CheckEscaped 检查是否离开了该迷宫
Walk 迷宫的下一步动作
OnStepFinsih 迷宫移动结束后的动作

很明显，在通过CheckEnter和CheckEscaped的组合判定迷宫生效后，通过覆盖Move的Walk和OnStepFinsih方法，来代替move的正常操作。
对于实际移动来说，根本不知道也不会去判断是路线规划返回的移动指令，还是迷宫返回的。
非常非常标准的一个补丁行为。
具体的Maze实现，可以参考
代码地址
]]></description><link>https://forum.hellclient.com/topic/35/深入浅出制作全自动mud机器人-迷宫</link><guid isPermaLink="true">https://forum.hellclient.com/topic/35/深入浅出制作全自动mud机器人-迷宫</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 03 Feb 2026 11:00:58 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-对象列表]]></title><description><![CDATA[对象(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+)\(([^\(\)]+)\)( \[.+\])?(( &lt;.+&gt;)*)$/
    //处理得到出口之后的信息(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 &gt; 4 &amp;&amp; output.startsWith("    ") &amp;&amp; output[4] != " ") {
                    return true
                }
                //未匹配过的行代表npc和道具结束
                return event.Context.Get("core.room.onobject")
            })
...

]]></description><link>https://forum.hellclient.com/topic/34/深入浅出制作全自动mud机器人-对象列表</link><guid isPermaLink="true">https://forum.hellclient.com/topic/34/深入浅出制作全自动mud机器人-对象列表</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 03 Feb 2026 10:42:03 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-路线规划配置]]></title><description><![CDATA[之前有提过，我的最新的机器人的路线规划都是通过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)

这个超时类，以便定位时被拦能超时重试。
]]></description><link>https://forum.hellclient.com/topic/33/深入浅出制作全自动mud机器人-路线规划配置</link><guid isPermaLink="true">https://forum.hellclient.com/topic/33/深入浅出制作全自动mud机器人-路线规划配置</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 03 Feb 2026 10:03:37 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-移动模块拆解]]></title><description><![CDATA[在移动模块介绍时我们说过，整个移动模块是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)

就很简单的拼装出了一个便利搜索的移动。
通过 基础模块(生产端)的 移动形态/类型 选项，和任务(消费端）,快速的组合出需要的移动模块。
]]></description><link>https://forum.hellclient.com/topic/32/深入浅出制作全自动mud机器人-移动模块拆解</link><guid isPermaLink="true">https://forum.hellclient.com/topic/32/深入浅出制作全自动mud机器人-移动模块拆解</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 03 Feb 2026 09:18:47 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-惯性导航]]></title><description><![CDATA[所谓的惯性导航，其实就是在机器人进行移动时，能推断出自己目前的位置。这样在移动中和移动结束时不会因为丢失位置信息而需要重新定位。
实现惯性导航的方式我见过不少。有根据地图计算下一步的房间的，甚至有监听发送的指令来进行计算的。
从我的理解来看，惯性导航，和真实的导航APP一样，代表一种预期。就是我走到这一步后，我应该在什么位置。
这是在路径计算时，已经计算好的预期。
如果行进中发生了意外，比如路线错误/被随机移动/被挡路，则应该将条件设置好后重新计算路线。
就如同导航APP的"已经选择新路线，新路线快/慢XX分钟"那样。
所以，在路线规划时，不管是移动，还是遍历，不光光应该获取到指令列表，还要获取到每个指令对应的目标，这样才能更好的进行位置计算。
当然，基于预计算的惯性导航也容易出问题，典型的就是位置计算错误后，会卡在某地。
但个人认为，显性的Bug比隐性的Bug好，容易复现的Bug会更容易解决。
]]></description><link>https://forum.hellclient.com/topic/31/深入浅出制作全自动mud机器人-惯性导航</link><guid isPermaLink="true">https://forum.hellclient.com/topic/31/深入浅出制作全自动mud机器人-惯性导航</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 03 Feb 2026 08:14:40 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-移动确认]]></title><description><![CDATA[在开始制作一个Mud机器人的移动模块之前，首先我们先要解决移动确认，也就是判断成功进入一个房间的问题。
稍微延展点的话，就是解析当前房间的信息

确定房间信息开始
抓取房间名和其他信息
抓取房间描述
抓取房间出口
抓取房间内对象
确定房间信息结束

本文章以我newhelljs的解析房间代码为例
代码地址
房间信息开始
一般房间信息开始未必是一个很明确的信号。
部分mud可能对房间有特殊的格式。但很多Mud单纯就是一个顶格不带特殊标点的短剧。
所以我在处理房间时是分为两个部分
第一是普通房间，比如
^[^，。！：.『』【】…“”？?&gt;.]{2,10}$

然后这里我使用了一个Filter的概念，就是不直接抛事件，而是在进行了预处理后，符合条件时才抛事件。对应的代码是：
    App.Engine.SetFilter("core.normalroomname", function (event) {
        let words = App.History.CurrentOutput.Words
        if (words.length == 1 &amp;&amp; words[0].Color != "" &amp;&amp; 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+)\(([^\(\)]+)\)( \[.+\])?(( &lt;.+&gt;)*)$/
    //处理得到出口之后的信息(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 &gt; 2 &amp;&amp; output.startsWith("  ") &amp;&amp; output[2] != " ") {
                    return true
                }
                //未匹配过的行代表npc和道具结束
                return event.Context.Get("core.room.onobject")
            })
        }, function (result) {
            if (result.Type != "cancel") {
                if (App.Map.Room.Name &amp;&amp; !App.Map.Room.ID) {
                    let idlist = App.Map.Data.RoomsByName[App.Map.Room.Name]
                    if (idlist &amp;&amp; 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处理函数进行处理，处理完毕后抛出事件通知需要后续操作的模块。
这样才能最大的程度的解耦合，并保证房间处理信息的内聚。
不做切割的话，很容易随着房间信息格式的调整，把整个代码搅乱。
]]></description><link>https://forum.hellclient.com/topic/30/深入浅出制作全自动mud机器人-移动确认</link><guid isPermaLink="true">https://forum.hellclient.com/topic/30/深入浅出制作全自动mud机器人-移动确认</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 03 Feb 2026 07:49:34 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-移动系统]]></title><description><![CDATA[在mud系统中，地图系统是最复杂的一个系统(相对于任务系统，心跳/属性系统，战斗系统)。
而且一般的中文Mud系统有没有普遍的Busy机制，部分Mud的wiz对添加地图也没有克制力，使得地图整个地图系统往往有超过手动玩家能力的复杂性。
所以移动系统也是整个Mud机器人中最重要和复杂的系统。
在这里我们也会分很多的章节去描述怎么做好Mud机器人里的移动系统。
当然，我们先要明白我们需要一个怎么样的移动系统
伪动态规划的移动系统
由于Mud地图的特性，在每个出口上经常有各种条件和限制。
地图的任务和NPC也经常有区域性的限制，所以我们往往需要有一个伪动态规划的地图系统。
为此我特地写了HellMapManager和hellmapmanger.ts 两个项目来更好的实现这点。
关于伪动态规划的内容参见这个链接
兼容迷宫的移动系统
一般我们地图规划其实是基于 一串固定指令对应一次移动的。
但很多时候我们需要引入迷宫系统，就是需要判断，处理，多次移动来构成一次虚拟移动的情况。
我称之为迷宫系统。
怎么兼容迷宫系统，怎么把移动移动和普通移动统一起来，这也是一个重要的课题。
可重计算重试的移动系统。
对于Mud机器人来说，一次移动其实也是一次预期。
预期的路线，和实际能行舟的路线可能是有区别的。
所以我们的移动系统应该是可重试，对不可进入的房间能屏蔽，能动态调整移动参数后继续尝试的系统。
]]></description><link>https://forum.hellclient.com/topic/27/深入浅出制作全自动mud机器人-移动系统</link><guid isPermaLink="true">https://forum.hellclient.com/topic/27/深入浅出制作全自动mud机器人-移动系统</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Mon, 02 Feb 2026 16:31:31 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-同步]]></title><description><![CDATA[同步是和异步相对的概念，一般来说，编码里的同步和异步的区别是在于是否阻塞。

同步的代码会阻塞并等待任务的返回结果。
异步的代码一般只发起任务，不关心任务是否完成，可以通过回调/通道之类的方法获取处理结果。

在我们的机器人中，一般会用异步的方式抛出一系列的指令，然后需要一个同步的指令，阻塞任务队列，保证不会被其他的信息污染，来进行更详细的指令判断。
比如，我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) =&gt; {
            task.AddTrigger(matcherTimes, (tri, result) =&gt; {
                if (result[1] == San.Data.WeaponName) {
                    task.Data = result[2]
                }
            })
            App.Send(`l ${San.Data.Weapon}`)
            App.Sync()
        },
        (result) =&gt; {
            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+)\(([^\(\)]+)\)( \[.+\])?(( &lt;.+&gt;)*)$/
    //处理得到出口之后的信息(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 &gt; 4 &amp;&amp; output.startsWith("    ") &amp;&amp; output[4] != " ") {
                    return true
                }
                //未匹配过的行代表npc和道具结束
                return event.Context.Get("core.room.onobject")
            })
        }, function (result) {
            if (result.Type != "cancel") {
                if (App.Map.Room.Name &amp;&amp; !App.Map.Room.ID) {
                    let idlist = App.Map.Data.RoomsByName[App.Map.Room.Name]
                    if (idlist &amp;&amp; idlist.length == 1) {
                        App.Map.Room.ID = idlist[0]
                    }
                }
                App.RaiseEvent(new App.Event("core.roomentry"))
            }
        })

是通过出现的行不是以4个空格开头(固定格式)来判断的。
]]></description><link>https://forum.hellclient.com/topic/26/深入浅出制作全自动mud机器人-同步</link><guid isPermaLink="true">https://forum.hellclient.com/topic/26/深入浅出制作全自动mud机器人-同步</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Mon, 02 Feb 2026 08:58:15 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-战斗系统]]></title><description><![CDATA[相对而言，我玩过的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&gt;#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&gt;#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&gt;#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&gt;#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的复杂配置就能完成了。
]]></description><link>https://forum.hellclient.com/topic/25/深入浅出制作全自动mud机器人-战斗系统</link><guid isPermaLink="true">https://forum.hellclient.com/topic/25/深入浅出制作全自动mud机器人-战斗系统</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Mon, 02 Feb 2026 08:21:11 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-任务编排]]></title><description><![CDATA[任务编排和用户队列其实是同一生态位，类似的上层建筑。
任务编排和用户队列的区别是

任务编排是宏观战略层面的，用户队列是微观战术层面的。
任务编排是面向多行文字变量的，用户队列是面向单行用户命令行输出的。
任务编排属于长期任务有与UI的交互，用户队列是临时任务基本与UI无交互。

任务定义
任务Quest,其实是一个很轻的元素。
    class Quest {
        constructor(id) {
            this.ID = id
        }
        InCooldown() {
            return (new Date()).getTime() &lt; 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&gt;&gt;tiejiang
maxexp 10000&gt;&gt;peiyao
!yueli 20&gt;&gt;beiqi
maxexp 29999&gt;&gt;letter
!yueli 2000&gt;&gt;beiqi
maxexp 100000&gt;&gt;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 &amp;&amp; ready.RunningQuest &amp;&amp; ready.RunningQuest.ID != Quest.ID) {
            return false
        }

任务编排带来的新问题
任务编排，本身就是为了自由度，自由扩展，解决
怎么才能完成更复杂的任务，获得更好的回报？
这个问题
同样的，也会引来新的复杂度
就是既然使用任务编排时我们一定会倾向于细化多分任务
那其他的相关设置怎么办？不如不同任务中不同的战斗设置？
所以，我们在配置时，不得不对所有相关的设置都引入条件指令格式。
增加了配置的复杂度。
鱼和熊掌不可兼得，莫过于此。
]]></description><link>https://forum.hellclient.com/topic/24/深入浅出制作全自动mud机器人-任务编排</link><guid isPermaLink="true">https://forum.hellclient.com/topic/24/深入浅出制作全自动mud机器人-任务编排</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 20 Jan 2026 10:38:32 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-用户队列]]></title><description><![CDATA[简而言之，用户队列，就是指令队列在用户输入框里的实现。
核心部分，之前都是纯代码层与用户无关的基础建筑，到了这里才算得上是上层建筑。
先看代码
代码地址
我们会发现，用户队列，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

虽然这种用户队列只能实现简单的，没有复杂任务的机器，但这是真正意义上的全自动机器人了。
甚至可以认为，用户队列的稳定性，等以你机器的主体的稳定性。
因此，用户队列对于主任务系统是一个独立的，迷你的系统。
但对于用户手动操作，以及机器的测试开发来说，还是有很大的意义的。
]]></description><link>https://forum.hellclient.com/topic/23/深入浅出制作全自动mud机器人-用户队列</link><guid isPermaLink="true">https://forum.hellclient.com/topic/23/深入浅出制作全自动mud机器人-用户队列</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 20 Jan 2026 10:05:36 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-指令队列]]></title><description><![CDATA[机器人的代码组织有很多种方式。
我自己的机器人其实经历过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) =&gt; {
        $.PushCommands(
            $.To("65"),
            $.Do(cmds.join("\n")),
            $.Do("i"),
            $.Wait(1000),
            $.Sync(),
            $.Function(Lianyao.Make)
        )
        $.Next()
    }

这个能很明显的看出队列的用法
规划一堆指令，Sync去订都执行完毕了，然后继续后续。
最后$.Next释放控制权
指令队列的挑战
指令队列或者其他纯代码逻辑控制的驱动方式，最大的挑战其实是内存泄漏。
比如如果要循环执行指令的话，不停PushCommands会堆很多很多层空队列，直到爆栈为止。
这是必须通过在最外层Insert指令，而不是PushCommands,才能避免内存泄漏
]]></description><link>https://forum.hellclient.com/topic/22/深入浅出制作全自动mud机器人-指令队列</link><guid isPermaLink="true">https://forum.hellclient.com/topic/22/深入浅出制作全自动mud机器人-指令队列</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 20 Jan 2026 09:33:33 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-条件指令]]></title><description><![CDATA[条件指令解决的是复杂Mud配置的问题。
Mud全自动机器人本质是一个典型的后台服务，和后台服务器一样，配置基本是多个纯文(变量)本来进行的。
同时，由于一般的Mud客户端的变量设置都没有语法支持，基本就是纯纯文本。所以会需要一个易写易读的配置格式。
这种情况下，我引入了条件指令的格式。
代码连接
具体格式为
条件1 条件参数1,!条件2&gt;#指令.参数 数据

逗号分隔多个条件,前置感叹号代表取反，必须所有条件都符合。
指令参数数据看具体的实现，先不深入。
具体使用什么条件，就是从上往下依次读，看条件匹配，匹配了，就执行，不继续执行(特殊配置可能继续匹配)。
不匹配看下一条。
这样人脑比较好解读。
同时，为了避免过于复杂的场合难以解读，还引入了可选的分组的概念
分组1:条件1 条件参数1,!条件2&gt;#指令.参数 数据

这样能直观的进行组别判断。
以我最复杂的战斗设置为例
大概是这么个画风
#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&gt;#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["经验"] &lt;= (data - 0)
    }))
    //注册yueli 条件
    App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("yueli", function (data, target) {
        return App.Data.Player.Score["阅历"] &gt;= (data - 0)
    }))
    //注册pot 条件
    App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("pot", function (data, target) {
        return App.Data.Player.HP["潜能"] &gt;= (data - 0)
    }))
    //注册quest 条件
    App.Quests.Conditions.RegisterMatcher(App.Quests.Conditions.NewMatcher("quest", function (data, target) {
        let rq = App.Quests.Running
        return rq &amp;&amp; rq.ID == data
    }))

]]></description><link>https://forum.hellclient.com/topic/21/深入浅出制作全自动mud机器人-条件指令</link><guid isPermaLink="true">https://forum.hellclient.com/topic/21/深入浅出制作全自动mud机器人-条件指令</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 20 Jan 2026 08:35:06 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-预期管理]]></title><description><![CDATA[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, () =&gt; {
                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,会直接失效，不会在下次连接时继续触发。
]]></description><link>https://forum.hellclient.com/topic/20/深入浅出制作全自动mud机器人-预期管理</link><guid isPermaLink="true">https://forum.hellclient.com/topic/20/深入浅出制作全自动mud机器人-预期管理</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 20 Jan 2026 07:57:53 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-事件总线]]></title><description><![CDATA[事件系统是一种很常见的代码结构。从web开发，GUI软件制作到各种系统，事件系统都是重要的组成部分。
事件总线是指将所有的事件的抛出与监听都挂在一个独立的类(EventBus或者EventBridge)上。
事件系统是天然的解耦合工具，底层代码抛出事件，处理代码接受事件，让人极容易的写出互相之间不依赖的代码。
写机器，自己嵌入一套事件系统是很顺理成章的事情，甚至某些客户端会在客户端内部实现一个全局的事件系统。
但是，就如同《人月神话》中提到过“没有银弹”。极度的自由，很容易带来极度的混乱。滥用事件很容易带来代码组织架构上的混乱，虽然解耦合了，但依然维护苦难。
毕竟我们强调解耦合是为了优化结构，降低维护难度。低耦合还有下半句高内聚。事件系统虽然实现了低耦合，但在内聚上做的一塌糊涂，必须极为注意使用。
事件的处理要有流向性
处理事件最怕的是循环调用，处理程序互相之间调用，织成一张网，如同乱成一团的线头一样，让人无法入手。
从我的角度，我一直推崇代码的架构要分层，比如我使用的一种架构
代码之间，要有严格的上下级关系，调用原则，避免相互依赖，依靠各种依赖注入/装饰类/代理类来实现反向的交互。
这种情况下，我们要有自我约束，事件的监听函数的调用过程，本质应该是一个水向下流，或者水蒸汽向上飘的单向过程。
事件的生产方和事件的消费方要有严格的上下关系。
这样事件的使用就会有头，有尾，能够梳理。
事件应该是业务的封装而非调用的信号
不同系统使用事件要解决的问题是不一样的。
对于Mud来说，事件应该是对实际发生的业务的抽象与封装
比如新的文字行，比如断开连接和建立连接，比如用户的点击操作等。
事件的生产方本质是把各种现实意义发生的元素，抽象成代码，封装成同一的格式，交给流水线。
不应该觉得为了调用一个处理函数，而抛一个事件。抛事件时一定要明确，很可能没有代码接受事件，或者有乱序的多个代码会接受事件。
如果需要明确的调用代码，不应该使用消息，应该使用显性的注册(依赖注入)机制，调用注册点中注册的代码，做明确的显性的调用。
事件处理函数不要抛出事件。
完整的说，事件处理函数，不要在同一个总线(bus,事件的监听系统)上抛出一个不同组的事件。
因为，每个事件，都会有0个或者多个处理函数，在这些处理函数上再抛出事件，会无法确定是否有循环调用，顺序问题以及依赖问题。
我认为唯一适合事件再抛出事件的，是对事件细化扩展为同组的子事件。
比如登陆成功事件，可以细化为 首次登陆 和 重新登陆事件
这就是同组的事件，(任意登陆/首次登陆/重新登陆)
监听程序会明确的监听其中一个，而不会每个都监听。
其他情况我不建议事件再触发时间，混乱度会字数成长。
总结
事件总线系统，是Mud机器人中必不可少的。
事件总线系统，极为强大。
事件总线系统，过于强大，容易混乱，必须有原则有策略的去规范使用。
]]></description><link>https://forum.hellclient.com/topic/19/深入浅出制作全自动mud机器人-事件总线</link><guid isPermaLink="true">https://forum.hellclient.com/topic/19/深入浅出制作全自动mud机器人-事件总线</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 20 Jan 2026 05:56:30 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-准备阶段]]></title><description><![CDATA[我觉得，全自动机器人本质是对人在Mud中行为的归纳，也就是常说的抽象。
抽象出的行为模式可能有很多种。
对于我而言，比较擅长的是
信息采集-&gt;准备阶段(状态重置)-&gt;任务执行

这样一个模型。
所以，继状态采集后,最重要的就是准备阶段。
什么是准备阶段？
准备阶段，粗泛点来说，就是通过不同的准备步骤将角色维护到一个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稳定的基础其实就是准备阶段。准备阶段大部分情况下是整个机器最复杂的部分，机器架构的很重要的一部分就是把复杂性统一放在准备阶段，这样任务部分就很轻很简单了。
同样，准备是一个很好的测试机器人稳定性的工具。
我们可以做一个空循环
信息采集-&gt;准备阶段-&gt;等一秒

空跑一段时间，就能验证整个机器人底层框架的稳定性，健壮性，以及是否有内存泄漏之类的问题了。
]]></description><link>https://forum.hellclient.com/topic/18/深入浅出制作全自动mud机器人-准备阶段</link><guid isPermaLink="true">https://forum.hellclient.com/topic/18/深入浅出制作全自动mud机器人-准备阶段</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Tue, 20 Jan 2026 03:51:45 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-主逻辑]]></title><description><![CDATA[对于一个机器人来说，并不需要一个主逻辑，完全可以多套逻辑并行作用。
但对于写机器人的你我来说，大脑还是更适合单线程的模式。所以在机器的合适的地方维护一套主逻辑，即降低开发的难度，又增加维护的效率，个人认为是性价比很高的一种方式。
主逻辑的入口
正常情况下，主逻辑是在正常运行时不调用的调用的。所以，在写主逻辑时，从什么地方进入，调用主逻辑，是我们首先要考虑的点。
定时器入口
其实，通过定时器/心跳来调用主逻辑，在我看来是正道。市面上的各种机器人，自动驾驶，本质也是在每个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弹出新指令的方式来运行代码。
我的建议
我的建议是，主逻辑的入口，尽量用程序回调这种依赖最少的方式调用。
具体的组织，根据代码的规模
用责任链(固定流程)或者指令队列缓冲(动态维护流程)+意外处理的方式组织。
]]></description><link>https://forum.hellclient.com/topic/16/深入浅出制作全自动mud机器人-主逻辑</link><guid isPermaLink="true">https://forum.hellclient.com/topic/16/深入浅出制作全自动mud机器人-主逻辑</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Mon, 19 Jan 2026 17:48:43 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-数据与缓存]]></title><description><![CDATA[虽然很多客户端和教程介绍机器时，都是用触发来驱动机器。
但是，要做一个稳定，功能强大的机器，被动的靠出发来驱动明显不可行，必须要依靠全面详尽的数据来运行。
在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 &lt; 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 = () =&gt; {
        App.RaiseEvent(eventBeforeCheck)//触发检查发送指令
        let checks = App.Checker.Check()//获取需要执行的检查
        if (checks.length == 0) {//无需检查
            App.Next()
            return
        }
        App.PushCommands(//一次性执行所有检查
            App.Commands.NewFunctionCommand(function () {
                checks.forEach(check =&gt; {
                    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 =&gt; {
                    check()
                });
                App.Next()
            }),
            App.NewSyncCommand(),
            App.Commands.NewFunctionCommand(function () {
                AfterCheck(id, context)
            }),
        )
        App.Next()
    }

这样就能确保每次在需要数据进行决策(准备)时，使用的是最新的有效的数据。
]]></description><link>https://forum.hellclient.com/topic/12/深入浅出制作全自动mud机器人-数据与缓存</link><guid isPermaLink="true">https://forum.hellclient.com/topic/12/深入浅出制作全自动mud机器人-数据与缓存</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Thu, 08 Jan 2026 13:47:49 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-全局单例]]></title><description><![CDATA[单例模式，是设计模式的一种。它是一种创建型设计模式，它的核心目标是确保一个类只有一个实例，并提供一个全局访问点来获取这个唯一的实例。
听着很牛吧？别管这个，这是被强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下
这样在写代码时，可以一目了然，方便理解和组织。
总结
对于代码来说，全局变量(单例)是必不可少的，但是太多的单例会让代码复杂度指数上升。通过定义一个合适的根单例，我们能更好的组织代码，提可高维护性。
]]></description><link>https://forum.hellclient.com/topic/11/深入浅出制作全自动mud机器人-全局单例</link><guid isPermaLink="true">https://forum.hellclient.com/topic/11/深入浅出制作全自动mud机器人-全局单例</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Thu, 08 Jan 2026 10:55:38 GMT</pubDate></item><item><title><![CDATA[深入浅出制作全自动Mud机器人-组件化]]></title><description><![CDATA[在全自动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变量,给每一个组件起一个唯一的字符串主键，将组件或者组件的生成函数都放在里面就行。
全局注册的目的是

在进行组件的替换/更新甚至顺序调整时，无需调整核心代码，只需要调整某个配置即可。
通过同一的调用方式，确保不会有其他位置的代码来直接调用组件内的代码，避免组件修改时出现全局问题。
方便的管理方式，能很容易的明白有多少个可用模块，不用去各个代码目录里寻找。

使用全局注册要注意的点是

只通过 注册的表 与组件交互，不建立直接处理的代码
在多个组件交互时，可以通过给组件加接口/全局事件/核心组里注册特殊毁掉的方式来处理。

重要的点
使用组件模式，其实是按照我们最初的目的来使用，也就是

将代码分为干净的核心代码，和脏(与业务更接近)的组件代码。确保代码指令控制在一个基本的可用度上。
通过配置，设置，能动态的调整激活的模块，实现更复杂的功能。

技术只是手段，代码如何处理，不一定要强求某个模式，还是为了实际目的服务。
]]></description><link>https://forum.hellclient.com/topic/10/深入浅出制作全自动mud机器人-组件化</link><guid isPermaLink="true">https://forum.hellclient.com/topic/10/深入浅出制作全自动mud机器人-组件化</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Thu, 08 Jan 2026 09:35:59 GMT</pubDate></item><item><title><![CDATA[我使用的一种机器人代码分层组织方式]]></title><description><![CDATA[在写gui程序时，我整理了一套代码组织方式
参看
HellMapManager项目就是按这个架构来组织的。
对于Mud机器人，我觉得这个组织形式也适用。当然，由于主要业务不同，住址形式也会不同。我只是将所有的代码根据对应的层次打个标签，避免不同层级的代码过于混淆。

UI 交互层 这个完全绑定于客户端的实现，在Mud机器人里也相对不重要，可以和Service层混在一起
Service 服务器层。从字面意义上来说，就是为最终用户提供的功能的封装层。UI交互的内容最终绑定到服务层上。最大的用途是防止用户UI直接操作到业务层，做封装和拦截，以及抽象。
Core 核心(业务) 层。对于Mud机器人来说，绝大部分的代码都是核心层。所以实际写机器人的时候Core肯定还要做细分，比如底层核心和任务模块。
Helper 辅助类，业务层和数据层直接的纽带，将数据的细节对业务做一定的封闭。
Adapters 适配器层 抽象底层交互。对于Mud机器人来说，就是将客户端的触发/别名/计时器做一个抽象，以及对应的事件Event框架。引入这个层的话能提升机器人可迁移性和做测试的可能。
Model 模型 在各个业务层中共同的数据结构。在Mud中比如房间，玩家，道具等等。
utils 工具层。比如中文转数字，格式化文字等纯与业务无关的，全局都可能使用的代码

]]></description><link>https://forum.hellclient.com/topic/9/我使用的一种机器人代码分层组织方式</link><guid isPermaLink="true">https://forum.hellclient.com/topic/9/我使用的一种机器人代码分层组织方式</guid><dc:creator><![CDATA[jarlyyn]]></dc:creator><pubDate>Thu, 08 Jan 2026 07:11:01 GMT</pubDate></item></channel></rss>