8 TransitionOn
zhbaor edited this page 2025-06-27 13:19:59 +08:00

transition 的两种写法

  • 旧的写法(称为传统 transition 写法)是在 transition() 函数里用 if - elif 判断场景、进行操作;
  • 新的写法(称为 TransitionOn 新写法)把每个场景的处理拆到单独的函数中,场景由 TransitionOn 装饰器的参数指定,函数中针对指定场景进行操作。

经过改造,框架目前同时兼容两种写法,以后由传统 transition 写法逐渐向 TransitionOn 写法过渡。

如果 solver 容器类定义了 solver_default_scene 属性,则为新写法,否则为旧的写法。

TransitionOn 新写法的优点

  1. 少了一层缩进;
  2. 不同场景的处理使用函数隔离,不容易出错;
  3. 可以通过返回值类型推断直观地看出 solver 容器可能在哪个场景下退出。

对已有代码的影响

变化主要影响调用栈追踪。传统 transition 写法只需追踪名为 transition 的函数。TransitionOn 新写法再追踪 transition 函数在哪里返回没有意义。

为简便起见,约定 TransitionOn() 装饰的函数名只能是 _

TransitionOn 装饰器的参数格式

  • 接收某一个具体的场景作为参数(例如 TransitionOn(Scene.INDEX)),表示用它装饰的函数处理参数中指定的场景;
  • 接收场景列表(例如 TransitionOn(BaseSolver.waiting_scene)),表示它装饰的函数负责处理列表中所有的场景。

TransitionOn 装饰器的默认参数含义与默认场景

solver_default_scene 的值称为默认场景

  • 默认场景可以是一个具体的场景:
    • 此时新 transition 遇到其它场景时,使用场景图导航跳转到默认场景;
    • TransitionOn() 在处理默认场景时可以省略参数。
  • 默认场景也可以是 None
    • 此时 TransitionOn() 不传参数时表示处理其它场景;
    • 必须有一个不传参数的 TransitionOn() 装饰的函数。

关于 TransitionOn 装饰器的实现

TransitionOn 要把函数与对应的场景关联起来。比较理想的办法是把函数存到 solver 容器的类属性里。虽然可以把装饰器写成 classmethod,但没法在类完成创建之前引用自己的类方法,所以不可行。如果尝试从被装饰的函数的参数列表入手,也不行:因为被装饰的是实例方法,对象实例化之后可以用 self.__class__ 获取类,但运行装饰器函数的时候,对象还没创建。好在可以用 __set_name__ 获取类。注意 __call__ 函数需要返回自身,这样 __set_name__ 才能被调用。

另一个问题是用 _ 作为函数名带来的。装饰多个名为 _ 的函数,会导致 __set_name__ 只在最后被调用一次,在此之前 __init____call__ 都被调用了若干次。所以先用类属性存储场景和函数,在调用 __set_name__ 时再一起处理。

TransitionOn 将场景与函数的映射存储到solver容器的类属性 transition_func 中。这里要注意,所有solver容器的类属性指向的是同一个字典,所以在 __set_name__ 中让类属性指向新的字典。

TransitionOn 也可以通过类装饰器实现。虽然我们不容易在solver类的创建过程中对它进行修改,但是可以通过装饰器在solver类创建结束后修改它。代码如下:

class Transition:
    scenes = []
    funcs = []
    default_scene = None

    @classmethod
    def on(cls, scene: int | list[int] | None = None):
        def decorator(func):
            if func.__name__ != "_":
                cls.scenes = []
                cls.funcs = []
                raise Exception('TransitionOn装饰的函数名必须是"_"')
            cls.scenes.append(scene)
            cls.funcs.append(func)

            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)

            return wrapper

        return decorator

    @classmethod
    def default(cls, scene: int | list[int] | None = None):
        cls.default_scene = scene

        def decorator(solver):
            solver.solver_default_scene = cls.default_scene
            solver.transition_func = {}
            for scene, func in zip(cls.scenes, cls.funcs):
                if scene is None:
                    scene = solver.solver_default_scene
                if not isinstance(scene, list):
                    scene = [scene]
                for s in scene:
                    solver.transition_func[s] = func
            cls.scenes = []
            cls.funcs = []
            return solver

        return decorator

以及对应的solver类写法:

@Transition.default(Scene.MISSION_DAILY)
class MissionSolver(BaseSolver):
    solver_name = "每日/每周任务"
    solver_max_duration = timedelta(minutes=2)

    def run(self) -> None:
        self.daily = False
        return super().run()

    @Transition.on()
    def _(self):
        if self.daily:
            self.tap("mission_weekly")
            return
        if pos := self.find("mission_collect"):
            self.tap(pos)
            return
        if self.animation(((0, 0), (560, 1080))):
            return
        self.daily = True
        self.solver_update_before_transition = False

    @Transition.on(Scene.MISSION_WEEKLY)
    def _(self):
        if not self.daily:
            self.tap("mission_daily")
            return
        if pos := self.find("mission_collect"):
            self.tap(pos)
            return
        if self.animation(((0, 0), (560, 1080))):
            return
        return True

我们更喜欢目前的实现方法。这种类装饰器的方法记录备用。

另外还有两种方法:使用metaclass或__init_subclass__