refactor normalize and resolution

This commit is contained in:
bridge
2026-01-06 22:13:47 +08:00
parent c266655af9
commit fbb32adbf6
14 changed files with 498 additions and 497 deletions

View File

@@ -5,10 +5,10 @@ from src.classes.action import InstantAction
from src.classes.action.targeting_mixin import TargetingMixin
from src.classes.event import Event
from src.classes.battle import decide_battle, get_effective_strength_pair
from src.classes.story_teller import StoryTeller
from src.classes.death import handle_death
from src.classes.death_reason import DeathReason
from src.classes.kill_and_grab import kill_and_grab
from src.utils.resolution import resolve_query
if TYPE_CHECKING:
from src.classes.avatar import Avatar
class Attack(InstantAction, TargetingMixin):
ACTION_NAME = "发起战斗"
@@ -24,7 +24,8 @@ class Attack(InstantAction, TargetingMixin):
IS_MAJOR: bool = True
def _execute(self, avatar_name: str) -> None:
target = self.find_avatar_by_name(avatar_name)
from src.classes.avatar import Avatar
target = resolve_query(avatar_name, self.world, expected_types=[Avatar]).obj
if target is None:
return
winner, loser, loser_damage, winner_damage = decide_battle(self.avatar, target)
@@ -42,11 +43,21 @@ class Attack(InstantAction, TargetingMixin):
self._last_result = (winner, loser, loser_damage, winner_damage)
def can_start(self, avatar_name: str) -> tuple[bool, str]:
_, ok, reason = self.validate_target_avatar(avatar_name)
return ok, reason
if not avatar_name:
return False, "缺少目标参数"
from src.classes.avatar import Avatar
target = resolve_query(avatar_name, self.world, expected_types=[Avatar]).obj
if target is None:
return False, "目标不存在"
if target.is_dead:
return False, "目标已死亡"
return True, ""
def start(self, avatar_name: str) -> Event:
target = self.find_avatar_by_name(avatar_name)
from src.classes.avatar import Avatar
target = resolve_query(avatar_name, self.world, expected_types=[Avatar]).obj
target_name = target.name if target is not None else avatar_name
# 展示双方折算战斗力(基于对手、含克制)
s_att, s_def = get_effective_strength_pair(self.avatar, target)
@@ -68,7 +79,8 @@ class Attack(InstantAction, TargetingMixin):
if not (isinstance(res, tuple) and len(res) == 4):
return []
target = self.find_avatar_by_name(avatar_name)
from src.classes.avatar import Avatar
target = resolve_query(avatar_name, self.world, expected_types=[Avatar]).obj
start_text = getattr(self, '_start_event_content', "")
from src.classes.battle import handle_battle_finish

View File

@@ -9,7 +9,10 @@ from src.classes.region import CityRegion
from src.classes.elixir import Elixir, get_elixirs_by_realm
from src.classes.prices import prices
from src.classes.cultivation import Realm
from src.utils.resolution import resolve_goods_by_name
from src.classes.weapon import Weapon
from src.classes.auxiliary import Auxiliary
from src.classes.item import Item
from src.utils.resolution import resolve_query
if TYPE_CHECKING:
from src.classes.avatar import Avatar
@@ -36,17 +39,19 @@ class Buy(InstantAction):
if not isinstance(region, CityRegion):
return False, "仅能在城市区域执行"
obj, obj_type, display_name = resolve_goods_by_name(target_name)
if obj_type == "unknown":
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Item])
if not res.is_valid:
return False, f"未知物品: {target_name}"
obj = res.obj
# 检查价格
price = prices.get_buying_price(obj, self.avatar)
if self.avatar.magic_stone < price:
return False, f"灵石不足 (需要 {price})"
# 丹药特殊限制
if obj_type == "elixir":
if isinstance(obj, Elixir):
elixir: Elixir = obj
# 必须是练气期丹药
@@ -66,35 +71,36 @@ class Buy(InstantAction):
return True, ""
def _execute(self, target_name: str) -> None:
obj, obj_type, display_name = resolve_goods_by_name(target_name)
if obj_type == "unknown":
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Item])
if not res.is_valid:
return
obj = res.obj
price = prices.get_buying_price(obj, self.avatar)
self.avatar.magic_stone -= price
# 交付
if obj_type == "elixir":
if isinstance(obj, Elixir):
self.avatar.consume_elixir(obj)
# TODO: 购买新装备,如果换下了旧装备,应该自动卖出
# 但是我现在还没有购买的能力,所以这个逻辑之后做。
elif obj_type == "item":
elif isinstance(obj, Item):
self.avatar.add_item(obj)
elif obj_type == "weapon":
elif isinstance(obj, Weapon):
# 购买装备需要深拷贝,因为装备有独立状态
new_weapon = copy.deepcopy(obj)
self.avatar.change_weapon(new_weapon)
elif obj_type == "auxiliary":
elif isinstance(obj, Auxiliary):
# 购买装备需要深拷贝
new_auxiliary = copy.deepcopy(obj)
self.avatar.change_auxiliary(new_auxiliary)
def start(self, target_name: str) -> Event:
obj, obj_type, display_name = resolve_goods_by_name(target_name)
res = resolve_query(target_name, expected_types=[Elixir, Weapon, Auxiliary, Item])
obj = res.obj
display_name = res.name
if obj_type == "elixir":
if isinstance(obj, Elixir):
action_desc = "购买并服用了"
elif obj_type in ["weapon", "auxiliary"]:
elif isinstance(obj, (Weapon, Auxiliary)):
action_desc = "购买并装备了"
else:
action_desc = "购买了"

View File

@@ -11,6 +11,7 @@ from src.classes.weapon import get_random_weapon_by_realm
from src.classes.auxiliary import get_random_auxiliary_by_realm
from src.classes.single_choice import handle_item_exchange
from src.utils.config import CONFIG
from src.utils.resolution import resolve_query
if TYPE_CHECKING:
from src.classes.avatar import Avatar
@@ -54,10 +55,11 @@ class Cast(TimedAction):
if not target_realm:
return False, "未指定目标境界"
try:
realm = Realm(target_realm)
except ValueError:
res = resolve_query(target_realm, expected_types=[Realm])
if not res.is_valid:
return False, f"无效的境界: {target_realm}"
realm = res.obj
cost = self._get_cost()
count = self._count_materials(realm)
@@ -68,7 +70,11 @@ class Cast(TimedAction):
return True, ""
def start(self, target_realm: str) -> Event:
self.target_realm = Realm(target_realm)
res = resolve_query(target_realm, expected_types=[Realm])
if res.is_valid:
self.target_realm = res.obj
self.target_realm = Realm(target_realm)
cost = self._get_cost()
# 扣除材料逻辑
@@ -87,9 +93,10 @@ class Cast(TimedAction):
for item, take in items_to_modify:
self.avatar.remove_item(item, take)
realm_val = self.target_realm.value if self.target_realm else target_realm
return Event(
self.world.month_stamp,
f"{self.avatar.name} 开始尝试铸造{target_realm}阶法宝。",
f"{self.avatar.name} 开始尝试铸造{realm_val}阶法宝。",
related_avatars=[self.avatar.id]
)

View File

@@ -3,8 +3,9 @@ from __future__ import annotations
from src.classes.action import InstantAction, Move
from src.classes.event import Event
from src.classes.action.move_helper import clamp_manhattan_with_diagonal_priority
from src.classes.region import Region, resolve_region
from src.classes.region import Region
from src.utils.distance import euclidean_distance
from src.utils.resolution import resolve_query
class MoveAwayFromRegion(InstantAction):
@@ -16,7 +17,10 @@ class MoveAwayFromRegion(InstantAction):
def _execute(self, region: str) -> None:
# 解析目标区域,并沿“远离该区域最近格点”的方向移动一步
r: Region = resolve_region(self.world, region)
r = resolve_query(region, self.world, expected_types=[Region]).obj
if not r:
return
x = self.avatar.pos_x
y = self.avatar.pos_y
# 找到目标区域内距离当前坐标最近的格点
@@ -35,19 +39,16 @@ class MoveAwayFromRegion(InstantAction):
Move(self.avatar, self.world).execute(dx, dy)
def can_start(self, region: str) -> tuple[bool, str]:
try:
resolve_region(self.world, region)
if resolve_query(region, self.world, expected_types=[Region]).obj:
return True, ""
except Exception:
return False, f"无法解析区域: {region}"
return False, f"无法解析区域: {region}"
def start(self, region: str) -> Event:
r = resolve_region(self.world, region)
return Event(self.world.month_stamp, f"{self.avatar.name} 开始离开 {r.name}", related_avatars=[self.avatar.id])
r = resolve_query(region, self.world, expected_types=[Region]).obj
region_name = r.name if r else region
return Event(self.world.month_stamp, f"{self.avatar.name} 开始离开 {region_name}", related_avatars=[self.avatar.id])
# InstantAction 已实现 step 完成
async def finish(self, region: str) -> list[Event]:
return []

View File

@@ -3,11 +3,12 @@ from __future__ import annotations
import random
from src.classes.action import DefineAction, ActualActionMixin
from src.classes.event import Event
from src.classes.region import Region, resolve_region
from src.classes.region import Region
from src.classes.sect_region import SectRegion
from src.classes.action import Move
from src.classes.action_runtime import ActionResult, ActionStatus
from src.classes.action.move_helper import clamp_manhattan_with_diagonal_priority
from src.utils.resolution import resolve_query
class MoveToRegion(DefineAction, ActualActionMixin):
@@ -46,8 +47,11 @@ class MoveToRegion(DefineAction, ActualActionMixin):
"""
移动到某个region
"""
region = resolve_region(self.world, region)
target_loc = self._get_target_loc(region)
target_region = resolve_query(region, self.world, expected_types=[Region]).obj
if not target_region:
return
target_loc = self._get_target_loc(target_region)
cur_loc = (self.avatar.pos_x, self.avatar.pos_y)
raw_dx = target_loc[0] - cur_loc[0]
@@ -58,29 +62,34 @@ class MoveToRegion(DefineAction, ActualActionMixin):
Move(self.avatar, self.world).execute(dx, dy)
def can_start(self, region: Region | str) -> tuple[bool, str]:
try:
r = resolve_region(self.world, region)
# 宗门总部限制:非本门弟子禁止入内
if isinstance(r, SectRegion):
if self.avatar.sect is None or self.avatar.sect.id != r.sect_id:
return False, f"{r.name}】是其他宗门驻地,你并非该宗门弟子。"
return True, ""
except Exception:
r = resolve_query(region, self.world, expected_types=[Region]).obj
if not r:
return False, f"无法解析区域: {region}"
# 宗门总部限制:非本门弟子禁止入内
if isinstance(r, SectRegion):
if self.avatar.sect is None or self.avatar.sect.id != r.sect_id:
return False, f"{r.name}】是其他宗门驻地,你并非该宗门弟子。"
return True, ""
def start(self, region: Region | str) -> Event:
r = resolve_region(self.world, region)
region_name = r.name
# 在开始时就确定目标点
self._get_target_loc(r)
return Event(self.world.month_stamp, f"{self.avatar.name} 开始移动向 {region_name}", related_avatars=[self.avatar.id])
r = resolve_query(region, self.world, expected_types=[Region]).obj
# 这里理论上在 can_start 已经校验过但为了安全再校验一次如果None则不处理实际上不会发生
if r:
region_name = r.name
# 在开始时就确定目标点
self._get_target_loc(r)
return Event(self.world.month_stamp, f"{self.avatar.name} 开始移动向 {region_name}", related_avatars=[self.avatar.id])
return Event(self.world.month_stamp, f"{self.avatar.name} 试图移动但目标无效", related_avatars=[self.avatar.id])
def step(self, region: Region | str) -> ActionResult:
self.execute(region=region)
r = resolve_region(self.world, region)
r = resolve_query(region, self.world, expected_types=[Region]).obj
if not r:
return ActionResult(status=ActionStatus.FAILED, events=[])
target_loc = self._get_target_loc(r)
# 完成条件:到达具体的随机目标点
@@ -91,5 +100,3 @@ class MoveToRegion(DefineAction, ActualActionMixin):
async def finish(self, region: Region | str) -> list[Event]:
return []

View File

@@ -6,7 +6,10 @@ from src.classes.action import InstantAction
from src.classes.event import Event
from src.classes.region import CityRegion
from src.classes.normalize import normalize_goods_name
from src.utils.resolution import resolve_goods_by_name
from src.utils.resolution import resolve_query
from src.classes.item import Item
from src.classes.weapon import Weapon
from src.classes.auxiliary import Auxiliary
class Sell(InstantAction):
@@ -29,32 +32,36 @@ class Sell(InstantAction):
return False, "仅能在城市区域执行"
# 使用通用解析逻辑获取物品原型和类型
obj, obj_type, _ = resolve_goods_by_name(target_name)
res = resolve_query(target_name, expected_types=[Item, Weapon, Auxiliary])
if not res.is_valid:
return False, f"未持有物品/装备: {target_name}"
obj = res.obj
normalized_name = normalize_goods_name(target_name)
# 1. 如果是物品,检查背包
if obj_type == "item":
if isinstance(obj, Item):
if self.avatar.get_item_quantity(obj) > 0:
pass # 检查通过
else:
return False, f"未持有物品: {target_name}"
# 2. 如果是兵器,检查当前装备
elif obj_type == "weapon":
elif isinstance(obj, Weapon):
if self.avatar.weapon and normalize_goods_name(self.avatar.weapon.name) == normalized_name:
pass # 检查通过
else:
return False, f"未持有装备: {target_name}"
# 3. 如果是辅助装备,检查当前装备
elif obj_type == "auxiliary":
elif isinstance(obj, Auxiliary):
if self.avatar.auxiliary and normalize_goods_name(self.avatar.auxiliary.name) == normalized_name:
pass # 检查通过
else:
return False, f"未持有装备: {target_name}"
else:
return False, f"未持有物品/装备: {target_name}"
return False, f"无法出售此类型: {target_name}"
return True, ""
@@ -63,26 +70,30 @@ class Sell(InstantAction):
if not isinstance(region, CityRegion):
return
# 使用通用解析逻辑获取物品原型和类型
obj, obj_type, _ = resolve_goods_by_name(target_name)
res = resolve_query(target_name, expected_types=[Item, Weapon, Auxiliary])
if not res.is_valid:
return
obj = res.obj
normalized_name = normalize_goods_name(target_name)
if obj_type == "item":
if isinstance(obj, Item):
quantity = self.avatar.get_item_quantity(obj)
self.avatar.sell_item(obj, quantity)
elif obj_type == "weapon":
elif isinstance(obj, Weapon):
# 需要再确认一次是否是当前装备
if self.avatar.weapon and normalize_goods_name(self.avatar.weapon.name) == normalized_name:
self.avatar.sell_weapon(obj)
self.avatar.change_weapon(None) # 卖出后卸下
elif obj_type == "auxiliary":
elif isinstance(obj, Auxiliary):
# 需要再确认一次是否是当前装备
if self.avatar.auxiliary and normalize_goods_name(self.avatar.auxiliary.name) == normalized_name:
self.avatar.sell_auxiliary(obj)
self.avatar.change_auxiliary(None) # 卖出后卸下
def start(self, target_name: str) -> Event:
obj, obj_type, display_name = resolve_goods_by_name(target_name)
res = resolve_query(target_name)
display_name = res.name if res.is_valid else target_name
return Event(
self.world.month_stamp,
f"{self.avatar.name} 在城镇出售了 {display_name}",

View File

@@ -1,10 +1,15 @@
from __future__ import annotations
from typing import Optional, Iterable
from typing import Optional, Iterable, TYPE_CHECKING
from src.classes.tile import get_avatar_distance
from src.classes.observe import get_observable_avatars
from src.classes.normalize import normalize_avatar_name
from src.utils.resolution import resolve_query
# 注意:避免在此处直接引入 Avatar 导致循环引用,使用 TYPE_CHECKING
if TYPE_CHECKING:
from src.classes.avatar import Avatar
class TargetingMixin:
@@ -17,14 +22,13 @@ class TargetingMixin:
"""
根据名字查找角色。
会自动规范化名字(去除括号等附加信息)以提高容错性。
例如:查找 "张三(元婴)" 会自动匹配到名为 "张三" 的角色
"""
normalized_name = normalize_avatar_name(name)
for v in self.world.avatar_manager.avatars.values():
if v.name == normalized_name:
return v
return None
# 动态导入 Avatar 类以进行类型检查,或者直接依赖 resolve_query 的内部逻辑
# 这里 resolve_query 需要 world 上下文
from src.classes.avatar import Avatar
res = resolve_query(name, self.world, expected_types=[Avatar])
return res.obj
def avatars_in_same_region(self, avatar: "Avatar") -> list["Avatar"]:
return self.world.avatar_manager.get_avatars_in_same_region(avatar)
@@ -78,5 +82,3 @@ class TargetingMixin:
if target.is_dead:
return None, False, "目标已死亡"
return target, True, ""

View File

@@ -7,13 +7,14 @@ from src.classes.mutual_action.mutual_action import MutualAction
from src.classes.event import Event
from src.classes.action.registry import register_action
from src.classes.action.cooldown import cooldown_action
from src.classes.region import resolve_region, CultivateRegion
from src.classes.region import CultivateRegion
from src.classes.action_runtime import ActionResult, ActionStatus
from src.classes.battle import decide_battle
from src.classes.story_teller import StoryTeller
from src.classes.death import handle_death
from src.classes.death_reason import DeathReason
from src.classes.action.event_helper import EventHelper
from src.utils.resolution import resolve_query
if TYPE_CHECKING:
from src.classes.avatar import Avatar
@@ -40,11 +41,17 @@ class Occupy(MutualAction):
def _get_region_and_host(self, region_name: str) -> tuple[CultivateRegion | None, "Avatar | None", str]:
"""解析区域并获取主人"""
region = resolve_region(self.world, region_name)
if region is None:
res = resolve_query(region_name, self.world, expected_types=[CultivateRegion])
# resolve_query 可能返回普通 Region这里需要严格检查是否为 CultivateRegion
region = res.obj
if not res.is_valid or region is None:
return None, None, f"无法找到区域:{region_name}"
if not isinstance(region, CultivateRegion):
return None, None, f"{region.name if region else '荒野'} 不是修炼区域,无法占据"
return region, region.host_avatar, ""
def can_start(self, region_name: str) -> tuple[bool, str]:

View File

@@ -2,29 +2,15 @@
名称规范化工具模块
提供统一的名称规范化函数,用于处理各类名称中的括号和附加信息。
适用于:角色名、地区名、物品名等。
"""
def _remove_parentheses(name: str) -> str:
def remove_parentheses(name: str, recursive: bool = False) -> str:
"""
通用括号移除函数:去除字符串中首个括号及其内容
支持的括号类型:() [] 【】 「」 『』 <> 《》
通用括号移除函数。
Args:
name: 原始字符串
Returns:
去除首个括号后的字符串(去除前后空格)
Examples:
>>> _remove_parentheses("张三(元婴)")
'张三'
>>> _remove_parentheses("青云林海(千年古松(金丹))")
'青云林海'
>>> _remove_parentheses("青云鹿角 -(练气)")
'青云鹿角 -'
recursive: 是否递归移除所有括号(处理嵌套括号)
"""
s = str(name).strip()
brackets = [
@@ -34,127 +20,56 @@ def _remove_parentheses(name: str) -> str:
("<", ">"), ("", "")
]
for left, right in brackets:
idx = s.find(left)
if idx != -1:
# 找到左括号,去除从此开始到字符串末尾的内容
s = s[:idx].strip()
break
return s
def normalize_avatar_name(name: str) -> str:
"""
规范化角色名字:去除括号及其中的附加信息(如境界)。
Args:
name: 原始角色名字,可能包含境界等附加信息
Returns:
规范化后的角色名字
Examples:
>>> normalize_avatar_name("张三(元婴)")
'张三'
>>> normalize_avatar_name("张三,境界:元婴")
'张三,境界:元婴'
"""
return _remove_parentheses(name)
def normalize_region_name(name: str) -> str:
"""
规范化地区名称:去除括号及其中的附加信息(如灵气密度、动植物等)。
处理多层括号:递归去除所有括号及其内容。
Args:
name: 原始地区名称,可能包含资源等附加信息
Returns:
规范化后的地区名称
Examples:
>>> normalize_region_name("太白金府金行灵气10")
'太白金府'
>>> normalize_region_name("青云林海(千年古松(金丹))")
'青云林海'
"""
s = str(name).strip()
brackets = [
("(", ")"), ("", ""),
("[", "]"), ("", ""),
("", ""), ("", ""),
("<", ">"), ("", "")
]
# 递归去除所有括号(用于处理嵌套括号)
while True:
found = False
for left, right in brackets:
# 查找最外层的左括号
start = s.find(left)
end = s.rfind(right)
if start != -1 and end != -1 and end > start:
s = (s[:start] + s[end + 1:]).strip()
if start != -1:
# 查找对应的右括号(从后往前找或者从前往后找匹配的)
# 简单策略:找最后一个右括号,或者找匹配的。
# 原有逻辑 region 使用的是 rfind这里我们采用更稳健的逻辑
# 既然是 remove通常是去除说明性文字保留主体。
# 策略:找到第一个左括号,和其对应的配对右括号(如果简单处理,直接找最后一个右括号可能误删)
# 但为了保持和原有 region 逻辑一致(处理 "青云林海(千年古松(金丹))" -> "青云林海"
# 只要发现左括号,就切断到末尾或者切断到匹配的右括号。
# 简化逻辑:找到第一个左括号,直接截断。这适用于绝大多数 "Name (Info)" 的情况。
s = s[:start].strip()
found = True
break
if not found:
if not recursive or not found:
break
return s
return s.strip()
def normalize_name(name: str) -> str:
"""
最通用的规范化:去除括号及其内容。
"""
return remove_parentheses(name)
# --- 兼容特定业务逻辑的别名或特化 ---
def normalize_avatar_name(name: str) -> str:
return remove_parentheses(name)
def normalize_region_name(name: str) -> str:
# 地区名可能包含多层嵌套,使用递归模式虽然在这里和非递归效果可能一样(因为都是截断),
# 但保持接口定义清晰。对于截断策略,递归其实没有意义,因为第一次就截断了。
# 除非括号在中间: "Region(Info) Suffix" -> "Region Suffix"
# 目前游戏里的命名习惯通常后缀是括号说明,所以直接截断是安全的。
return remove_parentheses(name)
def normalize_goods_name(name: str) -> str:
"""
规范化商品名称(包括物品、兵器、法宝、丹药)。
统一逻辑:
1. 移除括号及内容(如境界、类型说明)
2. 移除尾部的 " -" 标记(常见于材料生成名)
3. 移除首尾空格
Args:
name: 原始商品名称
Returns:
规范化后的商品名称
Examples:
>>> normalize_goods_name("青云鹿角 -(练气)") # item
'青云鹿角'
>>> normalize_goods_name("精铁剑(练气)") # weapon
'精铁剑'
>>> normalize_goods_name("聚气丹(练气)") # elixir
'聚气丹'
"""
s = _remove_parentheses(name)
s = s.rstrip(" -").strip()
return s
"""物品名额外去除尾部的 ' -'"""
s = remove_parentheses(name)
return s.rstrip(" -").strip()
def normalize_weapon_type(name: str) -> str:
"""
规范化兵器类型名称映射到标准的WeaponType枚举值。
处理格式:
- 去除空格和多余符号
- ""/"剑类"/"剑兵器" -> ""
Args:
name: 兵器类型名称
Returns:
规范化后的兵器类型名称WeaponType.value
Examples:
>>> normalize_weapon_type("")
''
>>> normalize_weapon_type("刀类")
''
"""
s = str(name).strip()
# 移除常见后缀
for suffix in ["", "兵器", "武器"]:
if s.endswith(suffix):
s = s[:-len(suffix)].strip()

View File

@@ -164,9 +164,6 @@ class NormalRegion(Region):
info = super().get_structured_info()
info["type_name"] = "普通区域"
# Fix: Return the actual structure instead of just calling get_structured_info on elements but never assigning
# The previous implementation (if it existed) was inherited from base or incorrect
# Assuming animals and plants are populated in __post_init__
info["animals"] = [a.get_structured_info() for a in self.animals] if self.animals else []
info["plants"] = [p.get_structured_info() for p in self.plants] if self.plants else []
@@ -244,66 +241,3 @@ class CityRegion(Region):
info = super().get_structured_info()
info["type_name"] = "城市区域"
return info
def _normalize_region_name(name: str) -> str:
s = str(name).strip()
brackets = [("(", ")"), ("", ""), ("[", "]"), ("", ""), ("", ""), ("", ""), ("<", ">"), ("", "")]
for left, right in brackets:
while True:
start = s.find(left)
end = s.rfind(right)
if start != -1 and end != -1 and end > start:
s = (s[:start] + s[end + 1:]).strip()
else:
break
return s
def resolve_region(world, region: Union[Region, str]) -> Region:
"""
解析字符串或 Region 为当前 world.map 中的 Region 实例
"""
from typing import Dict
if isinstance(region, str):
region_name = region
by_name: Dict[str, Region] = getattr(world.map, "region_names", {})
# 1) 精确匹配
r = by_name.get(region_name)
if r is not None:
return r
# 2) 归一化后再精确匹配
normalized = _normalize_region_name(region_name)
if normalized and normalized != region_name:
r2 = by_name.get(normalized)
if r2 is not None:
return r2
# 3) 唯一包含匹配
candidates = [name for name in by_name.keys() if name and (name in region_name or (normalized and name in normalized))]
if len(candidates) == 1:
return by_name[candidates[0]]
# 4) 兜底:若传入为宗门名,则解析为其总部区域
sect = sects_by_name.get(region_name) or (sects_by_name.get(normalized) if normalized and normalized != region_name else None)
if sect is not None:
sect_regions = getattr(world.map, "sect_regions", {}) or {}
matched = [r for r in sect_regions.values() if getattr(r, "sect_name", None) == sect.name]
if len(matched) == 1:
return matched[0]
if candidates:
sample = ", ".join(candidates[:5])
raise ValueError(f"区域名不唯一: {region_name},候选: {sample}")
raise ValueError(f"未知区域名: {region_name}")
if isinstance(region, Region):
by_id = getattr(world.map, "regions", None)
if isinstance(by_id, dict) and region.id in by_id:
return by_id[region.id]
return region
raise TypeError(f"不支持的region类型: {type(region).__name__}")

View File

@@ -6,7 +6,8 @@ from src.classes.world import World
from src.classes.avatar import Avatar, Gender
from src.classes.appearance import get_appearance_by_level
from src.classes.calendar import MonthStamp
from src.classes.region import Region, resolve_region
from src.classes.region import Region
from src.utils.resolution import resolve_query
from src.classes.cultivation import CultivationProgress
from src.classes.root import Root
from src.classes.age import Age
@@ -458,7 +459,8 @@ class AvatarFactory:
# 宗门弟子天生知道宗门总部位置
if avatar.sect is not None:
hq_region = resolve_region(world, avatar.sect.headquarter.name)
res = resolve_query(avatar.sect.headquarter.name, world, expected_types=[Region])
hq_region = res.obj
avatar.known_regions.add(hq_region.id)
if avatar.technique is not None:

View File

@@ -1,49 +1,192 @@
from typing import Any, Tuple, Optional
from __future__ import annotations
from typing import Any, Type, Optional, List, Union
from dataclasses import dataclass
from src.classes.normalize import normalize_goods_name
from src.classes.elixir import elixirs_by_name
from src.classes.weapon import weapons_by_name
from src.classes.auxiliary import auxiliaries_by_name
from src.classes.item import items_by_name
from src.classes.normalize import normalize_goods_name, normalize_name, normalize_avatar_name
from src.classes.elixir import elixirs_by_name, Elixir
from src.classes.weapon import weapons_by_name, Weapon
from src.classes.auxiliary import auxiliaries_by_name, Auxiliary
from src.classes.item import items_by_name, Item
from src.classes.cultivation import Realm
def resolve_goods_by_name(target_name: str) -> Tuple[Any, str, str]:
@dataclass
class ResolutionResult:
"""解析结果封装"""
obj: Any | None # 解析出的对象实例
resolved_type: Type | None # 对象的类型类
is_valid: bool # 是否成功
error_msg: str = "" # 错误信息
@property
def name(self) -> str:
"""尝试获取对象的名称"""
if self.obj and hasattr(self.obj, "name"):
return self.obj.name
return ""
def resolve_query(
query: Any,
world: Any = None,
expected_types: Optional[list[Type]] = None
) -> ResolutionResult:
"""
解析物品名称,返回 (对象, 类型, 显示名称)
如果未找到,返回 (None, "unknown", normalized_name)。
统一解析入口
类型字符串: "elixir", "item", "weapon", "auxiliary", "unknown"
查找顺序:
1. 丹药 (Elixir)
2. 兵器 (Weapon)
3. 辅助装备 (Auxiliary)
4. 普通物品 (Item)
Args:
query: 待解析的对象(字符串名 或 直接的对象实例)
world: 世界对象上下文(用于查找 Avatar, Region
expected_types: 期望的类型列表。如果提供,将优先或仅尝试这些类型。
支持的类型: Item, Weapon, Elixir, Auxiliary, Region, Avatar, Realm 等类对象
必须直接传入类对象,不再支持字符串名。
"""
normalized_name = normalize_goods_name(target_name)
if query is None:
return ResolutionResult(None, None, False, "查询为空")
# 0. 快速通道:如果已经是期望的对象实例
if expected_types:
for t in expected_types:
if isinstance(query, t):
return _success(query, t)
# 1. 尝试作为丹药查找
if normalized_name in elixirs_by_name:
# elixirs_by_name 返回的是 list我们取第一个作为对象
# 注意:对于购买/显示信息来说,取第一个通常是没问题的,
# 但如果有特定逻辑需要区分同名不同境界的丹药,可能需要更精细的处理。
# 这里保持原有逻辑。
elixir = elixirs_by_name[normalized_name][0]
return elixir, "elixir", elixir.name
# 如果不是字符串,且未命中上面的快速通道,可能输入了错误的对象
# 严格模式:既然未命中 expected_types 中的任何类型,也必然无法通过下面的字符串查找逻辑。
# 直接返回失败,除非 query 是字符串可以尝试查找。
if not isinstance(query, str):
return ResolutionResult(None, None, False, f"输入类型不支持且非字符串: {type(query)}")
# 2. 尝试作为兵器查找
weapon = weapons_by_name.get(normalized_name)
if weapon:
return weapon, "weapon", weapon.name
if not query:
return ResolutionResult(None, None, False, "查询字符串为空")
# 3. 尝试作为辅助装备查找
auxiliary = auxiliaries_by_name.get(normalized_name)
if auxiliary:
return auxiliary, "auxiliary", auxiliary.name
# 准备检查列表
checks = []
# 如果没有指定期望类型,则检查所有
if not expected_types:
checks = ["realm", "goods", "region", "avatar"]
else:
# 根据期望类型构建检查顺序
for t in expected_types:
# 必须是类对象
if not isinstance(t, type):
# 这里可以抛出异常,或者忽略。既然是重构,假设调用方已经修正。
continue
# 4. 尝试作为普通物品查找
item = items_by_name.get(normalized_name)
if item:
return item, "item", item.name
t_name = t.__name__
if t_name in ["Item", "Weapon", "Elixir", "Auxiliary"]:
if "goods" not in checks: checks.append("goods")
elif t_name == "Region" or t_name == "CityRegion" or t_name == "SectRegion" or t_name == "CultivateRegion":
if "region" not in checks: checks.append("region")
elif t_name == "Avatar":
if "avatar" not in checks: checks.append("avatar")
elif t_name == "Realm":
if "realm" not in checks: checks.append("realm")
return None, "unknown", normalized_name
# 执行检查
for check in checks:
if check == "realm":
res = _resolve_realm(query) # Realm 通常不需要 normalize 太多,或者在内部处理
if res: return _success(res, Realm)
elif check == "goods":
# 物品解析
obj = _resolve_goods(query)
if obj: return _success(obj, type(obj))
elif check == "region" and world:
obj = _resolve_region(query, world)
if obj: return _success(obj, type(obj))
elif check == "avatar" and world:
obj = _resolve_avatar(query, world)
if obj: return _success(obj, type(obj))
return ResolutionResult(None, None, False, f"无法解析: {query}")
def _success(obj: Any, type_cls: Type) -> ResolutionResult:
return ResolutionResult(obj, type_cls, True)
# --- 内部具体的解析逻辑 ---
def _resolve_goods(name: str) -> Any | None:
"""解析物品/装备/丹药"""
norm = normalize_goods_name(name)
# 1. 丹药 (返回列表中的第一个)
if norm in elixirs_by_name:
return elixirs_by_name[norm][0]
# 2. 兵器
if norm in weapons_by_name:
return weapons_by_name[norm]
# 3. 辅助
if norm in auxiliaries_by_name:
return auxiliaries_by_name[norm]
# 4. 普通物品
if norm in items_by_name:
return items_by_name[norm]
return None
def _resolve_realm(name: str) -> Realm | None:
"""解析境界"""
try:
# 尝试直接匹配值
return Realm(name)
except ValueError:
pass
# 尝试匹配枚举名
for r in Realm:
if r.name == name or r.value == name:
return r
return None
def _resolve_region(name: str, world: Any) -> Any | None:
"""解析区域"""
if not hasattr(world, 'map'):
return None
# 1. 精确匹配
by_name = getattr(world.map, "region_names", {})
if name in by_name:
return by_name[name]
# 2. 规范化匹配
norm = normalize_name(name)
if norm in by_name:
return by_name[norm]
# 3. 包含匹配 (如果有唯一解)
candidates = [k for k in by_name.keys() if (norm in k) or (name in k)]
if len(candidates) == 1:
return by_name[candidates[0]]
# 4. 宗门名称匹配 (解析到宗门驻地)
# 避免循环引用,这里动态导入或假设 sects_by_name 可用
from src.classes.sect import sects_by_name
sect = sects_by_name.get(name) or sects_by_name.get(norm)
if sect:
sect_regions = getattr(world.map, "sect_regions", {})
matched = [r for r in sect_regions.values() if getattr(r, "sect_name", None) == sect.name]
if len(matched) == 1:
return matched[0]
return None
def _resolve_avatar(name: str, world: Any) -> Any | None:
"""解析角色"""
if not hasattr(world, 'avatar_manager'):
return None
norm = normalize_avatar_name(name)
# 遍历查找 (性能注意:如果角色极多可能需要优化为字典查找)
# 假设 avatar_manager.avatars 是 dict[id, Avatar]
for avatar in world.avatar_manager.avatars.values():
if avatar.name == norm:
return avatar
return None