杰哥瞎扯蛋之移动模块上篇地图信息
以常见的武侠MUD来说,做机器人最核心的是3大模块1.地图模块,房间系统,涉及到所有移动。
2.用户信息模块,道具信息,涉及到用户的准备活动(确保在一个良好的状态)
3.战斗模块,应对战斗中所有的变数。
其中地图模块是基础中的基础。
这篇瞎扯蛋,主要是来盘一盘mud/mud机器人的地图/房间。
首先,我们要理解下,对于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);
}
这是扬州当铺
从代码和我们实际的游戏体验可以得出,每个房间有一些基本属性
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
很明显,对于一个地图模块来说,需要将地图关系类以合适的易于维护的格式储存起来。
同时,要和维护用户的状态道具一样,维护一份实时的当前房间信息清单,用来做代码驱动的辅助依据。
补一个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;
}
关于地图数据的维护
地图数据维护的核心是 不同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由于更新频繁不固定,我是直接做了个在线服务来进行查询的。并且不定期用街景机器人爬一圈,确保信息及时。
这和房间信息的关联是完全不同层次的两块内容,我不太喜欢都放在一起。
当前房间的维护
当前房间可以说重中之重,如果写模块时没有注意维护那以后会一直很蛋疼……
当前房间的维护主要就是在进入房间后进行重置(注意,进入房间和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的简单匹配(北侠的复杂的多,不过属于不能公开内容,就不展示了。)
{
"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方法取出第一个来处理或者判断有没有
或者直接遍历使用。
地图模块的第三个重要功能:当前状态状态。
一般来说,地图数据和当前房间信息是代码的基础。但是地图模块还有个非常重要的功能,维护当前的移动状态。
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移动的问题了。
基础部分差不多到这里了。
下篇应该是移动模块。
主要包含
1.移动的通用实现
2.迷宫和DFS模块
3.动态计算和重置,动态生成路径的 移动/多房间/固定路径遍历
4.移动的快照和恢复,可以在移动中插入战斗/手动移动,并还原继续原移动。
继续,不要停~
收获颇多!
页:
[1]
2