北大侠客行MUD论坛

 找回密码
 注册
搜索
热搜: 新手 wiki 升级
楼主: newstart

[PyMud]地图高级技巧-地形匹配与惯性导航在北侠中的实现

[复制链接]
发表于 2023-6-6 13:42:18 | 显示全部楼层
newstart 发表于 2023-6-6 01:32 PM
还好吧
做好每一个单独的Command模块就可以了
反正目前运行下来没啥问题 ...

异步肯定要判断是否成功啊……

成功成功处理,失败失败的处理……
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2023-6-6 13:50:03 | 显示全部楼层
jarlyyn 发表于 2023-6-6 01:42 PM
异步肯定要判断是否成功啊……

成功成功处理,失败失败的处理……

那是当然,异步情况下必须要处理
所以我用自己开发的客户端,实现了一个Command类,专门处理这种情况
Command相当于Alias+Trigger+Timer的组合

异步执行后,有4种不同状态结果,分别为成功、失败、重试、超时,所有的处理都要在Command类型里面完成的
app在https://github.com/crapex/pymud
异步Command可以看object.py中的SimpleCommand实现


所以,针对移动这个命令,我的实现对上述所有情况都进行了处理
self.cmd_move        = Configuration.CmdMove(self.session, self.mapper, id = "cmd_move", succ_tri = self.tri_loc, fail_tri = self.triggersInGroup("movefail"), retry_tri = self.triggersInGroup("moveretry"))


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
发表于 2023-6-6 14:23:47 | 显示全部楼层
newstart 发表于 2023-6-6 01:50 PM
那是当然,异步情况下必须要处理
所以我用自己开发的客户端,实现了一个Command类,专门处理这种情况
Com ...

这个应该是两个概念吧。

异步是异步,封装是封装。

异步一般目前比较常见的是类似js里Promoise的概念

https://developer.mozilla.org/en ... bal_Objects/Promise

  1. myPromise
  2.   .then((value) => `${value} and bar`)
  3.   .then((value) => `${value} and bar again`)
  4.   .then((value) => `${value} and again`)
  5.   .then((value) => `${value} and again`)
  6.   .then((value) => {
  7.     console.log(value);
  8.   })
  9.   .catch((err) => {
  10.     console.error(err);
  11.   });
复制代码


封装的话,我这里主要是把tri,alias,timer处理成envent,然后就是标准的有限状态擅长的领域了。

提这个主要是觉得1和2的作用域未必是完全匹配的。
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2023-6-6 15:20:17 | 显示全部楼层
本帖最后由 newstart 于 2023-6-6 03:28 PM 编辑
jarlyyn 发表于 2023-6-6 02:23 PM
这个应该是两个概念吧。

异步是异步,封装是封装。

可能是我表述的问题。
python的asyncio就是异步IO的标准库,和你说的promise是类似的,Python里面是使用asyncio.Future类来实现类似功能的
我前面说的是,Command相当于Alias+Trigger+Timer的组合,这里是相当于,确实可以理解成几个类型的封装,
但最大的核心区别,我的Trigger实现是既支持同步(回调函数模式),也支持异步(协程/事件循环模式)。
我在实现的Command类里面,所有对触发器的处理,都是使用异步事件模式实现的,其核心Command.execute方法也是使用async关键词修饰的,也是属于异步命令



同步情况下:
  1. if tri.matched(msg):
  2.    do_something
复制代码

异步情况下:
  1. class Trigger:
  2.    # trigger的异步等待方式
  3.    async def triggered(self):
  4.        self.event.clear()
  5.        await self.event.wait()
  6.        return trigger_capture_info
复制代码

在异步调用Trigger时,是使用下面的函数方式:
  1. async def execute():
  2.     write_some_command
  3.     await tri.triggered()
  4.     do_something
复制代码
执行上述execute函数只能在async定义的异步函数体中执行,或者使用asyncio.create_task创建任务,放到事件循环中执行。

python的asyncio库介绍:
https://docs.python.org/zh-cn/3.10/library/asyncio.html
Python异步协程(asyncio详解)
https://www.cnblogs.com/Red-Sun/p/16934843.html

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
发表于 2023-6-6 16:05:07 | 显示全部楼层
newstart 发表于 2023-6-6 03:20 PM
可能是我表述的问题。
python的asyncio就是异步IO的标准库,和你说的promise是类似的,Python里面是使用as ...

promise是promise,async是async。

js的异步发展史就是 callback->promise->async这条线的三个阶段。

promise的主要特点是可以串起来,可以有 解决/异常两条分路,以及可以储存和管理。

建立了promise并不一定执行

然后是async的问题

包括你引用的文档也写的很清楚

aysnc(和线程)解决的是并发问题。

并发问题是什么意思呢?

以房间名的触发为例。

  • 看见房间名,触发一个async,起了一个协程。
  • 又看到一次房间名,又触发一个sync,又起了一个携程。
  • 这时候你拥有了两个优先级相同的,并发的协程,都在等待npc信息。
  • 看到npc了。两个协程都触发了,会发两次kill,然后两个协程都结束。


我不知道你的客户端架构是怎么样的。但一般mush的lua机器人我是不建议用异步库的,容易出问题。

一般来说,我更建议和浏览器环境下的js一样,实际上的单线程,并发/协程由客户端主程序去维护,并防止数据竞争。



北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2023-6-6 19:24:32 | 显示全部楼层
本帖最后由 newstart 于 2023-6-6 07:30 PM 编辑
jarlyyn 发表于 2023-6-6 04:05 PM
promise是promise,async是async。

js的异步发展史就是 callback->promise->async这条线的三个阶段。

emmmm....以下继续进行技术探讨:)
我的理解和你的理解可能有些差异,并发是与并行来对应的;异步是与同步来对应的,这两者之间没有矛盾吧...
MushClient是不能很好的支持python异步的,如果lua可以用协程实现,但使用python时,只能使用基于生成器的协程(我以前就是这么干的),如果想使用async/await,由于两者之间不在同一个事件循环中(也就是不同的线程中),所以根本不能支持async/await语法运行
也是基于这个原因,我决定开发自己的客户端,以完整利用async/await的特性。

再解释下,我的客户端架构就是单线程的,异步单线程,事件循环由主对象PyMud进行维护;
客户端实现的Trigger的异步处理函数并非在Trigger被触发时调用,Trigger触发时仅会调用同步函数,在同步函数中,会设置其可等待的Future对象(我使用的asyncio.Event)的状态为done;
异步的async def triggered()仅在被函数代码await tri.triggered()调用时,才产生协程并予以运行...而这个等待的代码只有在Command的异步执行函数中才会被调用。
以你说的房间名举例:
tri.triggered()是一个协程,并不会在收到房间名时被调用,它仅会在Command的look指令发出后,被await tri.triggered()代码执行时才会被调用,以等待房间名,其状态直接会通过语句结果返回,举例:

  1.     class CmdLook(Command):
  2.          # 其他代码省略,仅留下execute执行函数
  3.          async def execute(self, cmd = "look", *args, **kwargs):
  4.                 try:
  5.                     self.reset()
  6.                     # 1. 输入命令
  7.                     self.tri_start.enabled = True
  8.                     self.tri_exits.reset()
  9.                     self.session.writeline(cmd)


  10.                     # 2. 等待房间出口被触发   
  11.                     done, pending = await asyncio.wait([asyncio.create_task(self.tri_exits.triggered()),], timeout = 5)
  12.                     if len(done) > 0:
  13.                         task = tuple(done)[0]
  14.                         state = task.result()
  15.                         # 此处原有room 的有关数据设定,此处删除了
  16.                         # 执行默认onSuccess
  17.                         self._onSuccess(room)
  18.                         result = self.SUCCESS
  19.                     else:
  20.                         # timeout,同success
  21.                         self._onTimeout()
  22.                         result = self.TIMEOUT
  23.                   
  24.                     self.reset()
  25.                     return self.State(result, room)
  26.                
  27.                 except Exception as e:
  28.                     self.error(f"异步执行中遇到异常, {e}, 类型为 {type(e)}")
  29.                     self.error(f"异常追踪为: {traceback.format_exc()}")
复制代码



这个不是与promise.then().catch()一样吗?
另外,你举例房间名触发的情况,当然,如果连续两次look,的确会触发两次协程,如果输入命令的速度过快,两次协程都会在第一次房间名出来的时候执行完毕,第二次则不会响应;
对于这个的处理方式,每一个Command实现时,其内部的awaitable对象都被存储,在下一次命令被执行时,如果上一个awaitable对象(如tri.triggered()协程)还处于pending状态,则会手动取消,以确保每一个Command只有一个awaitable对象起作用。
另外,我又在网上搜索了有关并发、并行、同步、异步的区别;以及promise、async/await的区别。js的新版中也是async/await,就如你说的js的异步发展史就是 callback->promise->async这条线的三个阶段。
网上说所,js中,async/await更像是一种语法糖,async定义的函数就是一个promise对象,是promise对象的改良版

参考资料:
1.1、程序中所谓「异步」和「并发」的区别有哪些:https://worktile.com/kb/ask/38122.html
1.2、区别:并行、并发、同步、异步: https://blog.csdn.net/wangxiaosu0501/article/details/124602000
2.1、promise和async await区别(我觉得这一篇写的特别完整):https://www.jianshu.com/p/0431e209dc0f
2.2、Promise和async/await的区别:https://blog.csdn.net/weixin_47277125/article/details/123754080
2.3、promise和async await的区别:https://blog.csdn.net/weixin_44246717/article/details/118659810
2.4、promise和async await的区别:https://blog.csdn.net/sinat_36728518/article/details/107293428?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-107293428-blog-118659810.235^v38^pc_relevant_anti_vip_base&spm=1001.2101.3001.4242.1&utm_relevant_index=1

北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
回复 支持 1 反对 0

使用道具 举报

发表于 2023-6-6 19:48:37 | 显示全部楼层
newstart 发表于 2023-6-6 07:24 PM
emmmm....以下继续进行技术探讨:)
我的理解和你的理解可能有些差异,并发是与并行来对应的;异步是与同步 ...

在我的理解里

promise 只是一个数据结构(callback的队列)。并不需要执行。
promise可以用async去消费他,理论也可以用自己 方式自行消费。

提到async/协程的问题就是,协程在我理解里就是一个在同一个线程里分配cpu事件的机制,挂起当前协程。

而mud机器人如果使用挂起的机制的话,复杂度很高(牵涉到怎么保证取消挂起),所以我才会觉得有问题。


北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2023-6-6 21:36:52 | 显示全部楼层
本帖最后由 newstart 于 2023-6-6 09:41 PM 编辑
jarlyyn 发表于 2023-6-6 07:48 PM
在我的理解里

promise 只是一个数据结构(callback的队列)。并不需要执行。

我查了下,在js中,promise对象也是使用await语句来进行消费,与async定义的对象一样,而且js与python的代码逻辑几乎完全一致
所以上面是一样的啊,async定义的函数也只是一个数据结构,不需要执行,需要使用await去消费;
如async函数定义

  1. async def func():
  2.     print('aaa')

  3. async def main():
  4.     #下面这句不会调用函数输出aaa,只会生成一个包含func()函数的协程对象
  5.     obj = func()
  6.     #下面这句才会真正执行func函数,输出aaa
  7.     await obj

  8. # 主入口
  9. asyncio.run(main())
复制代码

协程对象obj只会执行一次,执行完毕后其状态会自动从pending变成done,然后无法再次运行


我理解你上面说的还是传统的协程实现,在lua里,这种实现方式是
使用coroutine.create()来创建,
使用coroutine.yield()来等待
使用coroutine.resume()来恢复

下面是我以前写的lua携程代码(MushClient)

  1. function Room:CatchStart(cmd, after)
  2.     if cmd == nil or cmd == "" then cmd = "look" end
  3.     if after == nil or after <= 0.1 then after = 0.1 end
  4.     self:Update()
  5.     Room.catch_co = coroutine.create(function()
  6.         world.EnableTriggerGroup("gps.catchroom",false)        
  7.         world.EnableTrigger("Room_delay",true)
  8.         world.EnableTrigger("Room_name",true)
  9.         world.DoAfter(after, cmd)
  10.         world.EnableTimer("Room_overtimer", true)
  11.         world.ResetTimer("Room_overtimer")
  12.         local r = coroutine.yield() --挂起
  13.         if r then
  14.             self:CatchEnd()
  15.         else
  16.             self:CatchFail()
  17.         end
  18.     end)
  19.   coroutine.resume(Room.catch_co)
  20. end
复制代码

在python以前的版本里,使用基于生成器的协程,是
带有yield指令的函数作为协程
使用next调用协程推进(恢复)

使用异常捕获StopIteration来确认其中止状态


下面是我以前写的python协程代码(MushClient)

    def _onSuccess(self, sender, args):
  •         try:
  •             self._generator.send((CommandState.Success, sender, args))
  •         except StopIteration:
  •             #print("the command: '%s' has executed successfully" % self._command)
  •             pass
  •                
  •     def _onFail(self, sender, args):
  •         try:
  •             self._generator.send((CommandState.Failed, sender, args))
  •         except StopIteration:
  •             #print("the command: '%s' hasn't executed failed" % self._command)   
  •             pass
  •    
  •     def _onTimeout(self, sender, args):
  •         try:
  •             self._generator.send((CommandState.Timeout, sender, args))
  •         except StopIteration:
  •             #print("the command: '%s' hasn't executed timeout" .% self._command)   
  •             pass
  •    
  •     def _beforeExecute(self, **params):
  •         '''
  •         default before execute:
  •           enable the group
  •         '''
  •         en = params.get('autoenable', True)
  •         self.Enable(en)
  •    
  •     def _afterExecute(self, state):
  •         self.Enable(False)
  •         self._state = state
  •         self._doEvent("AfterDone")  
  •         self._state = CommandState.NotStart

  •     def Enable(self, value = True):  
  •         self._enabled = value
  •         for tri in self._triggers.keys():
  •             self._triggers[tri].Enabled = value
  •    
  •     def _coroutine(self):
  •         state, _, _ = yield
  •         self._afterExecute(state)

  •     def Execute(self, cmd, **params):
  •         self._command = cmd                                         # command text
  •         self._beforeExecute(**params)
  •         self._generator = self._coroutine()
  •         next(self._generator)
  •         self.mush.Execute(self._command)
  • 复制代码
    在上面这几种情况下,挂起/恢复/取消机制非常复杂,写出来的代码基本没有可读性
    后来版本升级后,可以@asyncio.coroutine标记协程和yield from语法调用(3.3版为async/yield from),但还是很难编写和读懂代码逻辑

    因此,Python自3.5版引入了async/await语法(js是es7中引入的),就是希望将基于协程的异步实现的可以像同步函数一样
    其实现逻辑是将未来产生的对象使用Future类(一翻译为期物,指被期待的物体)进行封装,这样就可以由事件循环自动维护协程的运行/等待/中止状态

    下面是我现在写的异步代码(PyMUD,自动少林跳楼)

    1.        async def jumptower(self):
    2.             "跳楼轻功主循环"
    3.             result = await self._runto.execute('rt {}'.format(self.PLACE_TOWER))
    4.             if result == self.SUCCESS:
    5.                 self._triggers["skm_levelup"].enabled = True
    6.                 self._triggers["skm_towersq"].enabled = True
    7.                 self._triggers["skm_towerup"].enabled = True
    8.                 self.session.writeline("set brief 3")
    9.                
    10.                 dir = "enter"
    11.                 times = 0
    12.                 while True:
    13.                     awt_tasks = []
    14.                     awt_tasks.append(asyncio.create_task(self._triggers["skm_towersq"].triggered(), name = "tsk_towersq"))
    15.                     awt_tasks.append(asyncio.create_task(self._triggers["skm_towerup"].triggered(), name = "tsk_towerup"))
    16.                            
    17.                     #await self._move.execute(dir)
    18.                     self.session.writeline(dir)
    19.                     done, pending = await asyncio.wait(awt_tasks, return_when = "FIRST_COMPLETED")
    20.                     for task in list(pending):
    21.                         task.cancel()
    22.                     
    23.                     tasks_done = list(done)
    24.                     if  len(tasks_done) == 1:
    25.                         task = tasks_done[0]
    26.                         state, name, line, wildcards = task.result()
    27.                         if name == self._triggers["skm_towerup"].id:
    28.                             await asyncio.sleep(0.1)
    29.                             floor = wildcards[0]
    30.                             if floor == "七":
    31.                                 self.session.writeline("exert regenerate")
    32.                                 self.session.writeline("exert recover")
    33.                                 dir = "out"

    34.                                 times += 1
    35.                                 if times >= 100:
    36.                                     times = 0
    37.                                     await self._lifemisc.execute("feed")
    38.                             else:
    39.                                 dir = "up"

    40.                         elif name == self._triggers["skm_towersq"].id:
    41.                             # 判断技能等级是否已达上限
    42.                             if self._levelup:
    43.                                 self._levelup = False
    44.                                 await self._skills.execute("skills")        # 等待以使skills命令获取完相关技能等级
    45.                                 dodge = self.session.getVariable("dodge", None)
    46.                                 if dodge:
    47.                                     dodge_lvl = dodge[0]
    48.                                     dodge_max = dodge[1]
    49.                                     if dodge_lvl >= dodge_max:
    50.                                         self.info(f"基本轻功已达等级上限 {dodge_max}", '技能')
    51.                                         break
    52.                                        
    53.                             await asyncio.sleep(2)
    54.                             dir = "enter"

    55.                     if self._halted:
    56.                         self.info("跳楼被手动中止", '技能')
    57.                         break

    58.                 self._triggers["skm_levelup"].enabled = False
    59.                 self._triggers["skm_towersq"].enabled = False
    60.                 self._triggers["skm_towerup"].enabled = False
    61.                 return self.SUCCESS
    62.             
    63.             else:
    64.                 self.error(f"未抵达跳楼起始点 {self.PLACE_TOWER}, 请检查重试", '技能')
    65.                 return self.FAILURE
    复制代码


    不同的协程实现方式,都可以实现同样的功能,但是代码编写难度和可读性完全是天差地别

    在下面的js异步发展史中提到,promise本质上并没有解决js的回调地狱问题,里面这句话说的很好:
    async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步编程看起来像同步一样
    异步编程的最高境界就是不用关心他是不是异步,因此async/await被很多人为是异步编程的终极解决方案。



    异步编程 101:Python async await发展简史:https://blog.csdn.net/weixin_34397291/article/details/91401107
    js异步发展史:https://blog.csdn.net/qq_36850967/article/details/93745949

    北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
    发表于 2023-6-6 21:44:28 | 显示全部楼层
    newstart 发表于 2023-6-6 09:36 PM
    我查了下,在js中,promise对象也是使用await语句来进行消费,与async定义的对象一样,而且js与python的代 ...

    那我的疑惑是,这里真的需要协程么?
    举个例子,我的跳塔代码

    1. (function (App) {
    2.     const Round = 10
    3.     App.Quest.Tiaota = {}
    4.     App.Quest.Tiaota.Data = {}
    5.     App.Quest.Tiaota.Start = function (param) {
    6.         App.Quest.Tiaota.Data = {
    7.             Count: 0,
    8.         }
    9.         if (param) {
    10.             param = param.trim()
    11.         }
    12.         let max = 0
    13.         if (param) {
    14.             let pmax = param - 0
    15.             if (isNaN(pmax)) {
    16.                 throw "跳塔参数" + param + "必须为最大dodge等级"
    17.             }
    18.             max = pmax
    19.         }
    20.         let skill = App.Core.PlayerGetSkillByID("dodge")
    21.         if (skill != null) {
    22.             if (max <= 0) {
    23.                 max = skill.Max
    24.             }
    25.             if (skill.Level >= max) {
    26.                 App.Fail()
    27.                 return
    28.             }
    29.         }
    30.         App.Commands([
    31.             App.NewCommand('prepare', App.PrapareFull),
    32.             App.NewCommand("to", App.Options.NewWalk("sls-zlxy")),
    33.             App.NewCommand("nobusy"),
    34.             App.NewCommand("function", App.Quest.Tiaota.Tiao),
    35.             App.NewCommand('do', "skills"),
    36.             App.NewCommand("nobusy"),
    37.         ]).Push()
    38.         App.Next()
    39.     }
    40.     App.Quest.Tiaota.Tiao = function () {
    41.         App.Quest.Tiaota.Data.Count++
    42.         if (App.Quest.Tiaota.Data.Count > Round) {
    43.             App.Next()
    44.             return
    45.         }
    46.         if (!App.Stopped) {
    47.             App.Commands([
    48.                 App.NewCommand("move", App.Options.NewPath("enter;u;u;u;u;u;u;out")),
    49.                 App.NewCommand("nobusy"),
    50.                 App.NewCommand("function", App.Quest.Tiaota.Tiao),
    51.             ]).Push()
    52.         }
    53.         App.Next()
    54.     }
    55. })(App)
    复制代码
    App.Commands就是一个类似于Promise的结构。

    App.Next()就是执行队列。

    有个App.Fail()就是进行失败处理。

    并不需要async机制。在mush里也可以用lua/js/python跑出来。
    北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
    发表于 2023-6-6 21:49:15 | 显示全部楼层
    高手论战
    已star,持续关注
    北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
    您需要登录后才可以回帖 登录 | 注册

    本版积分规则

    Archiver|手机版|小黑屋|北大侠客行MUD ( 京ICP备16065414号-1 )

    GMT+8, 2024-4-28 02:21 AM , Processed in 0.012215 second(s), 14 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2020, Tencent Cloud.

    快速回复 返回顶部 返回列表