From d8cb9389fb3044f6cb54e13ee90c5a78c2778079 Mon Sep 17 00:00:00 2001 From: bridge Date: Sat, 8 Nov 2025 02:42:28 +0800 Subject: [PATCH] add sect ranks --- src/classes/avatar.py | 49 ++++++-- src/classes/mutual_action/talk.py | 12 +- src/classes/sect.py | 60 +++++++++- src/classes/sect_ranks.py | 185 ++++++++++++++++++++++++++++++ src/sim/new_avatar.py | 85 ++++++++++++++ tests/test_sect_ranks.py | 165 ++++++++++++++++++++++++++ 6 files changed, 536 insertions(+), 20 deletions(-) create mode 100644 src/classes/sect_ranks.py create mode 100644 tests/test_sect_ranks.py diff --git a/src/classes/avatar.py b/src/classes/avatar.py index dc00eaa..857af13 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -1,10 +1,13 @@ import random from dataclasses import dataclass, field from enum import Enum -from typing import Optional, List +from typing import Optional, List, TYPE_CHECKING from collections import defaultdict import json +if TYPE_CHECKING: + from src.classes.sect_ranks import SectRank + from src.classes.calendar import MonthStamp from src.classes.action import Action from src.classes.action_runtime import ActionStatus, ActionResult @@ -89,6 +92,8 @@ class Avatar: alignment: Alignment | None = None # 所属宗门(可为空,表示散修/无门无派) sect: Sect | None = None + # 宗门职位(仅当有宗门时有效) + sect_rank: "SectRank | None" = None # 外貌(1~10级),创建时随机生成 appearance: Appearance = field(default_factory=get_random_appearance) # 装备的法宝(仅一个) @@ -185,9 +190,11 @@ class Avatar: relations_info = ";".join(relation_lines) if relation_lines else "无" magic_stone_info = str(self.magic_stone) + from src.classes.sect import get_sect_info_with_rank + if detailed: treasure_info = self.treasure.get_detailed_info() if self.treasure is not None else "无" - sect_info = self.sect.get_detailed_info() if self.sect is not None else "散修" + sect_info = get_sect_info_with_rank(self, detailed=True) alignment_info = self.alignment.get_detailed_info() if self.alignment is not None else "未知" region_info = region.get_detailed_info() if region is not None else "无" root_info = self.root.get_detailed_info() @@ -199,8 +206,8 @@ class Avatar: spirit_animal_info = self.spirit_animal.get_info() if self.spirit_animal is not None else "无" else: treasure_info = self.treasure.get_info() if self.treasure is not None else "无" - # personas和sect一致返回detailed,因为这俩太重要了 - sect_info = self.sect.get_detailed_info() if self.sect is not None else "散修" + # 宗门信息:非详细模式下只显示"宗门名+职位" + sect_info = get_sect_info_with_rank(self, detailed=False) region_info = region.get_info() if region is not None else "无" alignment_info = self.alignment.get_info() if self.alignment is not None else "未知" root_info = self.root.get_info() @@ -316,7 +323,7 @@ class Avatar: """ if self.current_action is None: return [] - # 记录当前动作实例引用,用于检测执行过程中是否发生了“抢占/切换” + # 记录当前动作实例引用,用于检测执行过程中是否发生了"抢占/切换" action_instance_before = self.current_action action = action_instance_before.action params = action_instance_before.params @@ -326,9 +333,14 @@ class Avatar: params_for_finish = filter_kwargs_for_callable(action.finish, params) finish_events = action.finish(**params_for_finish) # 仅当当前动作仍然是刚才执行的那个实例时才清空 - # 若在 step() 内部通过“抢占”机制切换了动作(如 Escape 失败立即切到 Battle),不要清空新动作 + # 若在 step() 内部通过"抢占"机制切换了动作(如 Escape 失败立即切到 Battle),不要清空新动作 if self.current_action is action_instance_before: self.current_action = None + # 动作完成后,如果有待执行计划,立即提交下一个(支持同月链式执行) + if self.has_plans(): + start_event = self.commit_next_plan() + if start_event is not None: + self._pending_events.append(start_event) if finish_events: # 允许 finish 直接返回事件(极少用),统一并入 pending for e in finish_events: @@ -338,13 +350,15 @@ class Avatar: for e in result.events: self._pending_events.append(e) events, self._pending_events = self._pending_events, [] - # 本轮已执行过,清除“新设动作”标记 - self._new_action_set_this_step = False + # 本轮已执行过,清除"新设动作"标记(但如果刚刚提交了新动作,commit_next_plan会重新设置为True) + if self.current_action is None: + # 当前无动作时才清除标记,避免清除新提交动作的标记 + self._new_action_set_this_step = False return events def update_cultivation(self, new_level: int): """ - 更新修仙进度,并在境界提升时更新寿命 + 更新修仙进度,并在境界提升时更新寿命和宗门职位 """ old_realm = self.cultivation_progress.realm self.cultivation_progress.level = new_level @@ -353,6 +367,9 @@ class Avatar: # 如果境界提升了,更新寿命期望 if self.cultivation_progress.realm != old_realm: self.age.update_realm(self.cultivation_progress.realm) + # 如果有宗门,检查是否需要晋升职位 + from src.classes.sect_ranks import check_and_promote_sect_rank + check_and_promote_sect_rank(self, old_realm, self.cultivation_progress.realm) def death_by_old_age(self) -> bool: """ @@ -595,9 +612,19 @@ class Avatar: def get_sect_str(self) -> str: """ - 获取宗门显示名:有宗门则返回宗门名,否则返回"散修"。 + 获取宗门显示名:有宗门则返回"宗门名+职位",否则返回"散修"。 + 例如:"合欢宗长老"、"散修" """ - return self.sect.name if self.sect is not None else "散修" + if self.sect is None: + return "散修" + + # 有宗门但无职位(理论上不应该出现,兜底处理) + if self.sect_rank is None: + return self.sect.name + + from src.classes.sect_ranks import get_rank_display_name + rank_name = get_rank_display_name(self.sect_rank, self.sect) + return f"{self.sect.name}{rank_name}" def set_relation(self, other: "Avatar", relation: Relation) -> None: """ diff --git a/src/classes/mutual_action/talk.py b/src/classes/mutual_action/talk.py index b52df5c..97de506 100644 --- a/src/classes/mutual_action/talk.py +++ b/src/classes/mutual_action/talk.py @@ -53,12 +53,12 @@ class Talk(MutualAction): ) EventHelper.push_pair(accept_event, initiator=self.avatar, target=target, to_sidebar_once=True) - # 立即启动 Conversation - from .conversation import Conversation - conv = Conversation(self.avatar, self.world) - conv.start(target_avatar=target) - # 直接执行一次 step,启动异步调用 - conv.step(target_avatar=target) + # 将 Conversation 加入计划队列,在Talk完成后立即执行 + self.avatar.load_decide_result_chain( + [("Conversation", {"target_avatar": target.name})], + self.avatar.thinking, + self.avatar.objective + ) else: # 拒绝攀谈 reject_event = Event( diff --git a/src/classes/sect.py b/src/classes/sect.py index fdec7fe..edf06c1 100644 --- a/src/classes/sect.py +++ b/src/classes/sect.py @@ -44,8 +44,8 @@ class Sect: weight: float = 1.0 # 影响角色或系统的效果 effects: dict[str, object] = field(default_factory=dict) - # 功法:在technique.csv中配置 - # TODO:宗内等级和称谓 + # 宗门自定义职位名称(可选):SectRank -> 名称 + rank_names: dict[str, str] = field(default_factory=dict) def get_info(self) -> str: hq = self.headquarter @@ -55,6 +55,20 @@ class Sect: # 详细描述:风格、阵营、驻地 hq = self.headquarter return f"{self.name}(阵营:{self.alignment},风格:{self.member_act_style},驻地:{hq.name})" + + def get_rank_name(self, rank: "SectRank") -> str: + """ + 获取宗门的职位名称(支持自定义) + + Args: + rank: 宗门职位枚举 + + Returns: + 职位名称字符串 + """ + from src.classes.sect_ranks import SectRank, DEFAULT_RANK_NAMES + # 优先使用自定义名称,否则使用默认名称 + return self.rank_names.get(rank.value, DEFAULT_RANK_NAMES.get(rank, "弟子")) def _split_names(value: object) -> list[str]: raw = "" if value is None or str(value) == "nan" else str(value) sep = CONFIG.df.ids_separator @@ -136,4 +150,44 @@ def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]: # 导出:从配表加载 sect 数据 -sects_by_id, sects_by_name = _load_sects() \ No newline at end of file +sects_by_id, sects_by_name = _load_sects() + + +def get_sect_info_with_rank(avatar: "Avatar", detailed: bool = False) -> str: + """ + 获取包含职位的宗门信息字符串 + + Args: + avatar: 角色对象 + detailed: 是否包含宗门详细信息(阵营、风格、驻地等) + + Returns: + - 散修:返回"散修" + - detailed=False:返回"明心剑宗长老" + - detailed=True:返回"明心剑宗长老(阵营:正,风格:...,驻地:...)" + """ + from typing import TYPE_CHECKING + if TYPE_CHECKING: + from src.classes.avatar import Avatar + + # 散修直接返回 + if avatar.sect is None: + return "散修" + + # 获取职位+宗门名(如"明心剑宗长老") + sect_rank_str = avatar.get_sect_str() + + # 如果不需要详细信息,直接返回职位字符串 + if not detailed: + return sect_rank_str + + # 需要详细信息:拼接宗门的详细描述 + sect_detail = avatar.sect.get_detailed_info() # "明心剑宗(阵营:正,...)" + + # 提取括号及其内容 + if "(" in sect_detail: + detail_part = sect_detail[sect_detail.index("("):] + return f"{sect_rank_str}{detail_part}" + + # 如果没有括号(理论上不应该出现),直接返回职位字符串 + return sect_rank_str \ No newline at end of file diff --git a/src/classes/sect_ranks.py b/src/classes/sect_ranks.py new file mode 100644 index 0000000..c225df8 --- /dev/null +++ b/src/classes/sect_ranks.py @@ -0,0 +1,185 @@ +""" +宗门等级系统 +定义宗门内的职位等级及其与修仙境界的映射关系 +""" +from enum import Enum +from functools import total_ordering +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from src.classes.cultivation import Realm + from src.classes.sect import Sect + + +@total_ordering +class SectRank(Enum): + """ + 宗门职位等级 + 从高到低:掌门 > 长老 > 内门弟子 > 外门弟子 + """ + Patriarch = "patriarch" # 掌门 + Elder = "elder" # 长老 + InnerDisciple = "inner" # 内门弟子 + OuterDisciple = "outer" # 外门弟子 + + def __lt__(self, other): + if not isinstance(other, SectRank): + return NotImplemented + # 数字越小职位越高,所以比较时反过来 + return RANK_ORDER[self] > RANK_ORDER[other] + + def __le__(self, other): + if not isinstance(other, SectRank): + return NotImplemented + return RANK_ORDER[self] >= RANK_ORDER[other] + + def __gt__(self, other): + if not isinstance(other, SectRank): + return NotImplemented + # 数字越小职位越高,所以比较时反过来 + return RANK_ORDER[self] < RANK_ORDER[other] + + def __ge__(self, other): + if not isinstance(other, SectRank): + return NotImplemented + return RANK_ORDER[self] <= RANK_ORDER[other] + + +# 宗门职位顺序(数字越小职位越高) +RANK_ORDER = { + SectRank.Patriarch: 0, + SectRank.Elder: 1, + SectRank.InnerDisciple: 2, + SectRank.OuterDisciple: 3, +} + + +# 默认职位名称(可被宗门自定义覆盖) +DEFAULT_RANK_NAMES = { + SectRank.Patriarch: "掌门", + SectRank.Elder: "长老", + SectRank.InnerDisciple: "内门弟子", + SectRank.OuterDisciple: "外门弟子", +} + + +def get_rank_from_realm(realm: "Realm") -> SectRank: + """ + 根据修仙境界映射为宗门职位 + + 映射规则: + - 练气 → 外门弟子 + - 筑基 → 内门弟子 + - 金丹 → 长老 + - 元婴 → 掌门(需要额外检查唯一性) + + Args: + realm: 修仙境界 + + Returns: + 对应的宗门职位 + """ + from src.classes.cultivation import Realm + + mapping = { + Realm.Qi_Refinement: SectRank.OuterDisciple, + Realm.Foundation_Establishment: SectRank.InnerDisciple, + Realm.Core_Formation: SectRank.Elder, + Realm.Nascent_Soul: SectRank.Patriarch, + } + return mapping.get(realm, SectRank.OuterDisciple) + + +def get_rank_display_name(rank: SectRank, sect: Optional["Sect"] = None) -> str: + """ + 获取职位的显示名称(支持宗门自定义) + + Args: + rank: 宗门职位 + sect: 宗门对象(可选,如果提供则使用其自定义名称) + + Returns: + 职位的显示名称 + """ + if sect is not None: + custom_name = sect.get_rank_name(rank) + if custom_name: + return custom_name + return DEFAULT_RANK_NAMES.get(rank, "弟子") + + +def should_auto_promote(old_realm: "Realm", new_realm: "Realm") -> bool: + """ + 判断境界突破后是否应该自动晋升宗门职位 + + Args: + old_realm: 旧境界 + new_realm: 新境界 + + Returns: + 是否应该晋升 + """ + if old_realm == new_realm: + return False + + from src.classes.cultivation import Realm + + # 检查境界是否提升 + old_rank = get_rank_from_realm(old_realm) + new_rank = get_rank_from_realm(new_realm) + + # 只有当新境界对应的职位更高时才晋升(职位枚举中 > 表示更高) + return new_rank > old_rank + + +def check_and_promote_sect_rank(avatar: "Avatar", old_realm: "Realm", new_realm: "Realm") -> None: + """ + 检查境界突破后是否需要晋升宗门职位,并执行晋升 + + Args: + avatar: 要检查的角色 + old_realm: 旧境界 + new_realm: 新境界 + """ + # 无宗门或无职位,不需要晋升 + if avatar.sect is None or avatar.sect_rank is None: + return + + # 检查是否应该晋升 + if not should_auto_promote(old_realm, new_realm): + return + + new_rank = get_rank_from_realm(new_realm) + + # 如果新职位是掌门,需要检查该宗门是否已有掌门 + if new_rank == SectRank.Patriarch: + if sect_has_patriarch(avatar): + # 已有掌门,只能晋升为长老 + new_rank = SectRank.Elder + + # 执行晋升 + avatar.sect_rank = new_rank + + +def sect_has_patriarch(avatar: "Avatar") -> bool: + """ + 检查当前宗门是否已有掌门(不包括自己) + + Args: + avatar: 要检查的角色 + + Returns: + 是否已有其他掌门 + """ + if avatar.sect is None: + return False + + # 从world中查找同宗门的其他avatar + for other in avatar.world.avatar_manager.avatars.values(): + if other is avatar: + continue + if other.sect == avatar.sect and other.sect_rank == SectRank.Patriarch: + return True + + return False + diff --git a/src/sim/new_avatar.py b/src/sim/new_avatar.py index fd82b9c..27dc26c 100644 --- a/src/sim/new_avatar.py +++ b/src/sim/new_avatar.py @@ -209,6 +209,9 @@ def build_mortal_from_plan(world: World, current_month_stamp: MonthStamp, *, nam # 位置刷新 avatar.tile = world.map.get_tile(avatar.pos_x, avatar.pos_y) + + # 分配宗门职位(根据境界) + _assign_sect_rank(avatar, world) # 写关系(父母/师徒);不发放宗门法宝 if plan.parent_avatar is not None: @@ -446,6 +449,9 @@ def build_avatars_from_plan( avatars_by_index[i] = avatar avatars_by_id[avatar.id] = avatar + + # 批量分配宗门职位(需要在所有avatar创建后统一处理,以正确检查掌门唯一性) + _assign_sect_ranks_batch(avatars_by_index, world) for (a, b), relation in planned_relations.items(): av_a = avatars_by_index[a] @@ -743,3 +749,82 @@ def get_new_avatar_with_config( return avatar + +def _assign_sect_rank(avatar: Avatar, world: World) -> None: + """ + 为单个avatar分配宗门职位(根据境界) + 处理掌门唯一性:如果该宗门已有掌门,元婴修士只能当长老 + + Args: + avatar: 要分配职位的角色 + world: 世界对象 + """ + # 散修无职位 + if avatar.sect is None: + avatar.sect_rank = None + return + + from src.classes.sect_ranks import get_rank_from_realm, sect_has_patriarch, SectRank + + # 根据境界获取对应职位 + rank = get_rank_from_realm(avatar.cultivation_progress.realm) + + # 如果是掌门,检查该宗门是否已有掌门 + if rank == SectRank.Patriarch: + if sect_has_patriarch(avatar): + # 已有掌门,降为长老 + rank = SectRank.Elder + + avatar.sect_rank = rank + + +def _assign_sect_ranks_batch(avatars: List[Avatar], world: World) -> None: + """ + 批量为avatars分配宗门职位 + 确保每个宗门只有一个掌门(按境界等级优先,同境界随机) + + Args: + avatars: 要分配职位的角色列表 + world: 世界对象 + """ + from src.classes.sect_ranks import get_rank_from_realm, SectRank + + # 先为所有人分配基础职位 + for avatar in avatars: + if avatar is None: + continue + if avatar.sect is None: + avatar.sect_rank = None + else: + avatar.sect_rank = get_rank_from_realm(avatar.cultivation_progress.realm) + + # 收集每个宗门的元婴修士(应为掌门的候选人) + sect_nascent_souls: Dict[int, List[Avatar]] = {} + for avatar in avatars: + if avatar is None or avatar.sect is None: + continue + if avatar.sect_rank == SectRank.Patriarch: + sect_id = avatar.sect.id + if sect_id not in sect_nascent_souls: + sect_nascent_souls[sect_id] = [] + sect_nascent_souls[sect_id].append(avatar) + + # 检查world中已存在的掌门 + existing_patriarchs: Dict[int, bool] = {} + for other in world.avatar_manager.avatars.values(): + if other.sect is not None and other.sect_rank == SectRank.Patriarch: + existing_patriarchs[other.sect.id] = True + + # 为每个宗门选择唯一掌门 + for sect_id, candidates in sect_nascent_souls.items(): + # 如果world中已有掌门,所有候选人都降为长老 + if existing_patriarchs.get(sect_id, False): + for avatar in candidates: + avatar.sect_rank = SectRank.Elder + else: + # 选择等级最高的作为掌门,其余降为长老 + candidates.sort(key=lambda av: av.cultivation_progress.level, reverse=True) + # 第一个保持掌门 + for avatar in candidates[1:]: + avatar.sect_rank = SectRank.Elder + diff --git a/tests/test_sect_ranks.py b/tests/test_sect_ranks.py new file mode 100644 index 0000000..b1e1e93 --- /dev/null +++ b/tests/test_sect_ranks.py @@ -0,0 +1,165 @@ +""" +测试宗门等级系统 +""" +import pytest +from src.classes.cultivation import CultivationProgress, Realm +from src.classes.sect_ranks import ( + SectRank, + get_rank_from_realm, + get_rank_display_name, + should_auto_promote, +) +from src.classes.sect import sects_by_name +from src.classes.world import World +from src.classes.map import Map +from src.classes.calendar import MonthStamp +from src.sim.new_avatar import make_avatars + + +def test_rank_from_realm(): + """测试境界到宗门职位的映射""" + assert get_rank_from_realm(Realm.Qi_Refinement) == SectRank.OuterDisciple + assert get_rank_from_realm(Realm.Foundation_Establishment) == SectRank.InnerDisciple + assert get_rank_from_realm(Realm.Core_Formation) == SectRank.Elder + assert get_rank_from_realm(Realm.Nascent_Soul) == SectRank.Patriarch + + +def test_rank_display_name(): + """测试职位显示名称""" + assert get_rank_display_name(SectRank.Patriarch) == "掌门" + assert get_rank_display_name(SectRank.Elder) == "长老" + assert get_rank_display_name(SectRank.InnerDisciple) == "内门弟子" + assert get_rank_display_name(SectRank.OuterDisciple) == "外门弟子" + + +def test_rank_comparison(): + """测试职位比较""" + assert SectRank.Patriarch > SectRank.Elder + assert SectRank.Elder > SectRank.InnerDisciple + assert SectRank.InnerDisciple > SectRank.OuterDisciple + assert SectRank.OuterDisciple < SectRank.Patriarch + + +def test_auto_promote(): + """测试自动晋升逻辑""" + assert should_auto_promote(Realm.Qi_Refinement, Realm.Foundation_Establishment) == True + assert should_auto_promote(Realm.Foundation_Establishment, Realm.Core_Formation) == True + assert should_auto_promote(Realm.Core_Formation, Realm.Nascent_Soul) == True + assert should_auto_promote(Realm.Qi_Refinement, Realm.Qi_Refinement) == False + + +def test_avatar_sect_rank_assignment(): + """测试avatar创建时宗门职位分配""" + from src.run.create_map import create_cultivation_world_map + game_map = create_cultivation_world_map() + world = World( + map=game_map, + month_stamp=MonthStamp(100 * 12), + ) + + # 创建多个avatar + avatars_dict = make_avatars(world, count=20, current_month_stamp=MonthStamp(100 * 12)) + avatars = list(avatars_dict.values()) + + # 检查所有有宗门的avatar都有职位 + for avatar in avatars: + if avatar.sect is not None: + assert avatar.sect_rank is not None, f"{avatar.name} 有宗门但没有职位" + # 职位应该匹配境界 + expected_rank = get_rank_from_realm(avatar.cultivation_progress.realm) + # 如果不是预期职位,只可能是元婴修士被降为长老(因为掌门唯一性) + if avatar.sect_rank != expected_rank: + assert avatar.cultivation_progress.realm == Realm.Nascent_Soul + assert avatar.sect_rank == SectRank.Elder + else: + assert avatar.sect_rank is None, f"{avatar.name} 散修不应该有职位" + + +def test_patriarch_uniqueness(): + """测试每个宗门只有一个掌门""" + from src.run.create_map import create_cultivation_world_map + game_map = create_cultivation_world_map() + world = World( + map=game_map, + month_stamp=MonthStamp(100 * 12), + ) + + # 创建足够多的avatar + avatars_dict = make_avatars(world, count=50, current_month_stamp=MonthStamp(100 * 12)) + avatars = list(avatars_dict.values()) + + # 统计每个宗门的掌门数量 + sect_patriarchs = {} + for avatar in avatars: + if avatar.sect is not None and avatar.sect_rank == SectRank.Patriarch: + sect_id = avatar.sect.id + if sect_id not in sect_patriarchs: + sect_patriarchs[sect_id] = [] + sect_patriarchs[sect_id].append(avatar.name) + + # 确保每个宗门最多只有一个掌门 + for sect_id, patriarchs in sect_patriarchs.items(): + assert len(patriarchs) <= 1, f"宗门 {sect_id} 有多个掌门: {patriarchs}" + + +def test_sect_str_display(): + """测试宗门信息显示""" + from src.run.create_map import create_cultivation_world_map + game_map = create_cultivation_world_map() + world = World( + map=game_map, + month_stamp=MonthStamp(100 * 12), + ) + + avatars_dict = make_avatars(world, count=20, current_month_stamp=MonthStamp(100 * 12)) + avatars = list(avatars_dict.values()) + + for avatar in avatars: + sect_str = avatar.get_sect_str() + if avatar.sect is None: + assert sect_str == "散修" + else: + # 应该包含宗门名 + assert avatar.sect.name in sect_str + # 应该包含职位名 + if avatar.sect_rank is not None: + rank_name = get_rank_display_name(avatar.sect_rank, avatar.sect) + assert rank_name in sect_str + + +def test_cultivation_breakthrough_promotion(): + """测试突破境界后自动晋升""" + from src.run.create_map import create_cultivation_world_map + game_map = create_cultivation_world_map() + world = World( + map=game_map, + month_stamp=MonthStamp(100 * 12), + ) + + avatars_dict = make_avatars(world, count=10, current_month_stamp=MonthStamp(100 * 12)) + avatars = list(avatars_dict.values()) + + # 找一个练气期的宗门弟子 + target_avatar = None + for avatar in avatars: + if avatar.sect is not None and avatar.cultivation_progress.realm == Realm.Qi_Refinement: + target_avatar = avatar + break + + if target_avatar is not None: + # 记录原职位 + old_rank = target_avatar.sect_rank + assert old_rank == SectRank.OuterDisciple + + # 突破到筑基 + target_avatar.cultivation_progress.level = 31 # 筑基初期 + target_avatar.update_cultivation(31) + + # 检查职位是否晋升 + assert target_avatar.sect_rank == SectRank.InnerDisciple + assert target_avatar.sect_rank > old_rank + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) +