jarlyyn 发表于 2024-10-21 16:43:28

杰哥瞎扯蛋之移动模块上篇地图信息

以常见的武侠MUD来说,做机器人最核心的是3大模块

1.地图模块,房间系统,涉及到所有移动。
2.用户信息模块,道具信息,涉及到用户的准备活动(确保在一个良好的状态)
3.战斗模块,应对战斗中所有的变数。

其中地图模块是基础中的基础。

这篇瞎扯蛋,主要是来盘一盘mud/mud机器人的地图/房间。

jarlyyn 发表于 2024-10-21 16:50:56

首先,我们要理解下,对于MUD来说,地图是什么,房间是什么。

让我找出一份20年前的某MUD代码(非北侠)。

地图大概是这么样的一个布局


大体上按区域布局,一个房间一个文件。对于一个房间来说,"/d/[区域名]/[房间名].c"大体可以认为是一个为一ID。

每个房间大体是这样的

inherit ROOM;

void create()
{
      set("short", "中央广场");
      set("long", @LONG
这里是城市的正中心,一个很宽阔的广场,铺着青石地面。一些游手好
闲的人在这里溜溜达达,经常有艺人在这里表演。中央有一棵大榕树,盘根
错节,据传已有千年的树龄,是这座城市的历史见证。树干底部有一个很大
的树洞 (dong)。 你可以看到北边有来自各地的行人来来往往,南面人声鼎
沸,一派繁华景象,东边不时地传来朗朗的读书声,西边则见不到几个行人,
一片肃静。
LONG );
      set("no_sleep_room",1);
      set("outdoors", "city");
      set("item_desc", ([
                "dong" : "这是一个黑不溜湫的大洞,里面不知道有些什么古怪。\n",
      ]));

      set("exits", ([
                "east" : __DIR__"dongdajie1",
                "south" : __DIR__"nandajie1",
                "west" : __DIR__"xidajie1",
                "north" : __DIR__"beidajie1",
      ]));

      set("objects", ([
                __DIR__"npc/liapo" : 1,
      ]));

      setup();
}

void init()
{
      add_action("do_enter", "enter");
}

int do_enter(string arg)
{
      object me;
      me = this_player();

      if (! arg || arg == "")
                return 0;

      if (arg == "dong")
      {
                if (me->is_busy())
                        return notify_fail("你的动作还没有完成,不能移动。\n");

                message("vision",
                        me->name() + "一弯腰往洞里走了进去。\n",
                        environment(me), ({me}) );
                me->move("/d/gaibang/inhole");
                message("vision",
                        me->name() + "从洞里走了进来。\n",
                        environment(me), ({me}) );
                return 1;
      }
}      


上面是扬州广场

// Room: /city/dangpu.c
// YZC 1995/12/04

inherit ROOM;

void create()
{
        set("short", "当铺");
        set("long", @LONG
这是一家以买卖公平著称的当铺,一个五尺高的柜台挡在你的面
前,柜台上摆着一个牌子(paizi), 柜台后坐着唐老板,一双精明的
上上下下打量着你。
LONG
        );
        set("no_fight", 1);
        set("no_steal", 1);
        set("no_beg",1);
        set("item_desc", ([
                "paizi" : "公平交易\n
sell      卖
buy         买
redeem      赎
value       估价
",
        ]));
        set("objects", ([
                __DIR__"npc/tang" : 1,
        ]));
        set("exits", ([
                "west" : __DIR__"nandajie1",
                "down" : __DIR__"xsmidao",
        ]));

        setup();
}

int valid_leave(object me, string dir)
{
      if (dir == "down" && me->query("family/family_name") != "雪山寺")
                return notify_fail("唐楠眼睛一翻,道:干什么来了,想偷东西啊?\n");

      return ::valid_leave(me, dir);
}


这是扬州当铺

jarlyyn 发表于 2024-10-21 17:00:47

从代码和我们实际的游戏体验可以得出,每个房间有一些基本属性

1.唯一ID。当然,这个只有wiz可以看到,我们只能自己编(唯一标识)
2.出口列表,对应了房间和房间之间的直接关联。 开始方向->方向指令->目标房间,这是最简单的格式(常规出口)
3.特殊出口/指令。每个房间可以通过 add_action 添加指令,加入响应的特殊指令,执行 me->move 指令,进行移动(特殊的移动)
4.移动验证指令valid_leave,在移动时加入条件判断,确定一个出口是否能成功使用(移动的条件)
5.objects,房间对象的列表(道具清单)
6.特殊设置,比如是否是室外,是否是可以睡觉/灌水等等(特殊属性)
7.其他文字描述。
按照我们实际使用的目的,其实这类信息分为两类

1.地图关系类,就是所谓的点对点地图,不管是不是用户的当前房间都要关注,比如 1,2,3,4,6
2.房间信息类,就是当前房间的信息,决定了用户和环境的交互,包括1,2,5,7

很明显,对于一个地图模块来说,需要将地图关系类以合适的易于维护的格式储存起来。

同时,要和维护用户的状态道具一样,维护一份实时的当前房间信息清单,用来做代码驱动的辅助依据。

jarlyyn 发表于 2024-10-21 17:01:43

补一个add_action的例子

//Room: xiaoshulin1.c 小树林
//Date: Oct. 2 1997 by That

inherit ROOM;
void create()
{
      set("short","小树林");
      set("long",@LONG
这是峨嵋山金顶华藏庵外的一片小树林。林中没有路,但地上依稀有些足
迹,似乎刚有人走过。北面有一扇小窗。
LONG);
      set("outdoors", "emei");
      set("exits",([ /* sizeof() == 1 */
          "south"   : __DIR__"xiaoshulin2",
      ]));
      set("no_clean_up", 0);
      setup();
}

void init()
{
      add_action("do_jump", "jump");
}
int do_jump(string arg)
{
      object me;
      if (!arg || arg !="chuang") return 1;
      me = this_player();
      message_vision("$N趁人不注意,跳进窗里。。\n",me);
      me->move(__DIR__"hcawest");
      message_vision("$N从华藏庵外跳窗进来。\n",me);
      return 1;
}


jarlyyn 发表于 2024-10-21 17:10:17

关于地图数据的维护

地图数据维护的核心是 不同ID房间之间的关联。

已知房间A,房间B,求两者关系C。

这个C可以是一个指令(east/west之类),可以有条件,符合条件才能进入(比如各门派专属路径),可以是迷宫(A地到B地之间是一个迷宫,包括所有非直接指令,比如武当新人下山可能被拦要去ask song),在计算路径时应该有权重(比如北侠那蜗牛马车)。

个人比较喜欢用MUD远古大神zsz使用的一套地图文件格式,如果你在其他mud用过mapper.exe的话可能也涉及过。我加入路径权重和反向标签额,但基本格式不变,大概是

0=中央广场|e:59,enter dong:1927,n:22,s:40,w:1,
1=西大街|e:0,n:2,s:5,w:7,
2=衙门大门|n·:3,s:1,
3=衙门正厅|e:1550,n:4,s:2,w:21,
4=内宅|s:3,
5=兵营大门|n:1,s·:6,
6=兵营|n:5,
7=西大街|e:1,n:9,s·:8,w:13,
8=扬州武馆|n:7,
9=财主大门|n:10,s:7,
10=财主大院|n:11,s:9,
11=财主后院|s:10,w·:12,n:2914,
12=财主西厢|e:11,
13=西门|e:7,w:14,n:2880,
14=西门大道|e:13,s·:15,w:19,
15=武道场|n:14,se:16,sw:18,
16=武道场|nw:15,sw:17,
17=武道场|ne:16,nw:18,
18=武道场|ne:15,se:17,
19=关洛道|e:14,w:20,
20=函谷关|e:19,s:77,w:244,
21=西厅|e:3,
22=北大街|e·:26,n:24,s:0,w:23,
23=钱庄|e:22,
24=北大街|e:27,n:34,s:22,w·:25,
25=武庙|e:24,nw:1552,u:1551,w:2900,
26=客店|menter0·:2046,s:1553,u·:-1,w:22,
27=醉仙楼|e·:28,u:29,w:24,
28=马厩|goto beijing:1353,w:27,
29=醉仙楼二楼|d:27,e·:30,
30=醉仙楼大堂|e:31,n:32,s:33,w:29,
31=玫瑰宴厅|w:30,
32=芙蓉宴厅|s:30,
33=牡丹宴厅|n:30,
34=北门|n:35,s:24,w:1558,
35=大驿道|n:36,s:34,
36=大驿道|n:37,s:35,
37=大驿道|e·:38,n:39,s:36,
38=小院|w:37,e:-1,
39=汉水南岸|sl1>cross:1073,sl1>sl>back:2817,s:37,e:2762,drive>yell boat。:2882,

(参考数据,明显不是北侠的地图。)

个人不在地图里记录太多信息,就算北侠版本的地图也就多记一个区域,房间名@区域的格式。

其他文字信息/固定npc由于更新频繁不固定,我是直接做了个在线服务来进行查询的。并且不定期用街景机器人爬一圈,确保信息及时。

这和房间信息的关联是完全不同层次的两块内容,我不太喜欢都放在一起。

jarlyyn 发表于 2024-10-21 17:16:04

当前房间的维护

当前房间可以说重中之重,如果写模块时没有注意维护那以后会一直很蛋疼……

当前房间的维护主要就是在进入房间后进行重置(注意,进入房间和look的行为时一样的,所以我有个特殊的标志位,通过#look可以保持房间主要信息不重置,不是新房间)

进入新房间后,清理所有信息,包括房间ID。然后客是分析和记录。

将各种数据(最重要的时房间对象列表)维护好后,确定房间信息结束,再抛出时间,进行各种处理,比如遍历的判断,移动成功后的房间ID变更,继续走下一个房间等等。

我在其他mud的房间信息处理代码

    App.BindEvent("core.roomname", App.Core.Room.OnName)
    reExit = /+/g
    App.Core.Room.OnExit = function (event) {
      event.Context.Propose(function () {
            let result = [...event.Data.Wildcards.matchAll(reExit)].map(data => data).sort()
            // App.Data.Room.Exits = result
            App.Map.Room.WithExits(result)
            App.RaiseEvent(new App.Event("room.onexit"))
            PlanOnExit.Execute()
      })
    }
    App.BindEvent("core.onexit", App.Core.Room.OnExit)
    let matcherOnHeal = /^    (\S{2,8})正坐在地下(修炼内力)。$/
    let matcherOnObj = /^    ((\S+) )?(\S*「.+」)?(\S+)\(([^\(\)]+)\)( \[.+\])?(( <.+>)*)$/
    var PlanOnExit = new App.Plan(App.Positions.Connect,
      function (task) {
            task.AddTrigger(matcherOnObj, function (trigger, result, event) {
                let item = new objectModule.Object(result, result, App.History.CurrentOutput).
                  WithParam("身份", result).
                  WithParam("外号", result).
                  WithParam("描述", result || "").
                  WithParam("状态", result || "").
                  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, "", App.History.CurrentOutput).
                  WithParam("动作", "result")
                App.Map.Room.Data.Objects.Append(item)
                event.Context.Set("core.room.onobject", true)
                return true
            })

            task.AddCatcher("line", function (catcher, event) {
                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
                  if (idlist && idlist.length == 1) {
                        App.Map.Room.ID = idlist
                  }
                }
                App.RaiseEvent(new App.Event("core.roomentry"))
            }
      })


很明显,重点在记录房间内的对象上,以及做了一个房间id的简单匹配(北侠的复杂的多,不过属于不能公开内容,就不展示了。)

jarlyyn 发表于 2024-10-21 17:20:28

{
"ID": "",
"Name": "钱庄",
"Zone": "",
"Exits": [
    "east"
],
"Data": {
    "Objects": {
      "Items": [
      {
          "Data": null,
          "ID": "Jjc",
          "Key": "",
          "IDLower": "jjc",
          "Label": "丐柒",
          "Params": {
            "身份": "丐帮第十八代传人",
            "描述": "",
            "状态": " <断线中>",
            "动作": ""
          },
          "Mode": 0
      },
      {
          "Data": null,
          "ID": "Gba",
          "Key": "",
          "IDLower": "gba",
          "Label": "霸丐",
          "Params": {
            "身份": "丐帮第十八代传人",
            "描述": "",
            "状态": "",
            "动作": ""
          },
          "Mode": 0
      },
      {
          "Data": null,
          "ID": "Qian yankai",
          "Key": "",
          "IDLower": "qian yankai",
          "Label": "钱眼开",
          "Params": {
            "外号": "钱庄老板「铁公鸡」",
            "描述": "",
            "状态": "",
            "动作": ""
          },
          "Mode": 0
      }
      ]
    }
}
}
我日常维护的房间数据,其中Items的Data是个懒加载的数据。


在这个模块下,我要找NPC是这样找的


    let Checker = function (wanted) {
      let result = map.Room.Data.Objects.FindByName(wanted.Target)
      for (var obj of result) {
            if (obj.ID.indexOf(" ") > 0) {
                if (MQ.Data.NPC && MQ.Data.NPC.Zone) {
                  MQ.Data.NPC.SetZone(MQ.Data.NPC.Zone)
                }
                return obj
            }
      }
      if (App.Map.Room.ID) {
            map.Room.Data.Objects.Items.forEach((item) => {
                if (item.ID.indexOf(" ") > 0 && item.Label.length < 5) {
                  App.Core.HelpFind.OnNPC(item.Label, item.ID, App.Map.Room.ID)
                }
            })
      }
      return null
    }


可以FindByName,FindByID,FindByLabel,FindByIDLower,取出符合条件的列表,通过First方法取出第一个来处理或者判断有没有

或者直接遍历使用。

jarlyyn 发表于 2024-10-21 17:34:18

地图模块的第三个重要功能:当前状态状态。

一般来说,地图数据和当前房间信息是代码的基础。但是地图模块还有个非常重要的功能,维护当前的移动状态。

MUD的地图是动态的,从我的角度来看, 纯静态的地图很不好用。

比如有些房间,男的可以进去,女的不能进。桃花的可以进去,其他的门派不能进去,轻功好的可以进去,轻功不好的不可以进去。

再比如,有些房间移动时可能发生意外,比如被拦路,比如季节时间不同,需要根据当前状态,动态生成和调整路径。

我的地图模块有两个函数

      FlashTags() {
            this.#tags = {}
            Mapper.flashtags()
            Mapper.ResetTemporary()
            this.#blocked = []
            this.#temporaryPaths=[]
      }



      InitTags() {
            this.FlashTags()
            if (this.Move != null) {
                this.Move.InitTags(this)
            }
            this.#tagsIniter.forEach(fn => {
                fn(this)
            })
            for (var key in this.#tags) {
                if (this.#tags) {
                  Mapper.settag(key, true)
                }
            }
            this.#temporaryPaths.forEach(tp => {
                Mapper.AddTemporaryPath(tp.from, tp.path)
            })
      }

在每次移动时清除移动状态,并调用注册的函数进行状态的初始化。

我先在用的特殊状态包括


[*]tag,作为最简单的开关接口,决定某个路径能或不能走。最典型的是性别,门派,轻功是否符合某个条件
[*]BlockedPath,拦截的路径,被NPC然接后,会调用移动的Retry方法,屏蔽当前的房间移动,重新计算遍历和行走路线
[*]Whitelist/Blacklist,可以选择移动只在固定的房间或者不在固定的房间内。
[*]TemparyPath,临时路径,比如有些路径需要随机刷的NPC传送的话,可以在当前房间有对应的NPC临时开启。


一般来说,用Tag和BlockedPath,配合移动的迷宫模块,能解决绝大部分的mud移动的问题了。

jarlyyn 发表于 2024-10-21 17:36:48

基础部分差不多到这里了。

下篇应该是移动模块。

主要包含

1.移动的通用实现
2.迷宫和DFS模块
3.动态计算和重置,动态生成路径的 移动/多房间/固定路径遍历
4.移动的快照和恢复,可以在移动中插入战斗/手动移动,并还原继续原移动。

slapyou 发表于 2024-11-6 13:09:33

继续,不要停~
收获颇多!
页: [1] 2
查看完整版本: 杰哥瞎扯蛋之移动模块上篇地图信息