path_find.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. #!/usr/bin/env python3
  2. """
  3. 996传奇客户端 A* 寻路算法 还原实现
  4. 来源: 二进制 aiManager::FindPath @0x10005xxxx + Lua PathFindAStar.lua 交叉对照
  5. (二进制 C++ 侧主路径 + Lua 侧 LPA* 变体备用, 见 §实现说明)
  6. 验证: 在 0139/3.map 上 e2e 寻路成功, 路径合法 (2026-09-23)
  7. 算法核心 (与 Lua PathFindAStar 一致):
  8. - 8 方向 (4 正 + 4 对角)
  9. - 启发式: 欧氏距离²
  10. - 代价: 直线 10 / 对角 14 (即 √2 ≈ 1.414 * 10)
  11. - 开/闭集: openlist (用 k 值排序), closed 集合
  12. - 对角穿越: 无显式阻止 (实测地图有此情况, 游戏规则允许)
  13. 局限 (与二进制 / Lua 一致):
  14. - 寻路范围被 48×32 包围盒限制 (disX=24, disY=16 中心化)
  15. - 超过此距离需多次寻路拼接 (PathFindController:FindPathAStar 的多段调用)
  16. - 不考虑动态障碍 (actor 阻挡由 checkAbleToWalk 在调用前过滤)
  17. 实现说明:
  18. - 本模块是 *可运行* 的 A* (不是 LPA*), 与 Lua 的 LPA* 变体在结果上等价
  19. (无地图变化场景下 LPA* == A*)
  20. - 直接消费 map_parser.MapData, 路径点以 (x, y) 列表返回
  21. """
  22. from __future__ import annotations
  23. import heapq
  24. import os
  25. import sys
  26. from typing import Optional, Iterator
  27. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  28. from map_parser import MapData, MapCell
  29. # ============== 算法常量 (与 Lua PathFindAStar 一致) ==============
  30. LINE_COST = 10 # 正交移动代价 (上/下/左/右)
  31. BIAS_COST = 14 # 对角移动代价 (NE/NW/SE/SW), ≈ LINE * √2
  32. # 默认寻路范围限制 (Lua: disX=24, disY=16, 中心化)
  33. DEFAULT_DIS_X = 24
  34. DEFAULT_DIS_Y = 16
  35. # ============== 数据结构 ==============
  36. class PathNode:
  37. """A* 节点: 含 f/g/h/backpointer"""
  38. __slots__ = ("x", "y", "g", "h", "f", "parent")
  39. def __init__(self, x: int, y: int, g: float = 0, h: float = 0,
  40. parent: Optional["PathNode"] = None):
  41. self.x = x
  42. self.y = y
  43. self.g = g # 起点到当前的实际代价
  44. self.h = h # 启发式代价 (当前到终点估计)
  45. self.f = g + h # 总代价 (优先队列排序键)
  46. self.parent = parent
  47. def __lt__(self, other: "PathNode") -> bool:
  48. return self.f < other.f
  49. def __eq__(self, other) -> bool:
  50. return isinstance(other, PathNode) and self.x == other.x and self.y == other.y
  51. def __hash__(self) -> int:
  52. return hash((self.x, self.y))
  53. # ============== 启发式与代价 ==============
  54. def heuristic(x1: int, y1: int, x2: int, y2: int) -> int:
  55. """欧氏距离² 启发式 (与 Lua cost = dx*dx + dy*dy + line 一致)"""
  56. dx = x1 - x2
  57. dy = y1 - y2
  58. return dx * dx + dy * dy
  59. def step_cost(from_cell: MapCell, to_cell: MapCell,
  60. from_x: int, from_y: int, to_x: int, to_y: int) -> float:
  61. """
  62. 移动代价 (与 Lua PathNode:cost 一致)
  63. 直线 (dx=0 or dy=0) = LINE_COST=10
  64. 对角 (dx≠0 and dy≠0) = BIAS_COST=14
  65. 障碍物: 任意一格是障碍 → 不可走 (用 INFINITY 表示, 见 caller)
  66. """
  67. dx = abs(to_x - from_x)
  68. dy = abs(to_y - from_y)
  69. if dx == 0 or dy == 0:
  70. return LINE_COST
  71. return BIAS_COST
  72. # ============== 邻居生成 ==============
  73. NEIGHBOR_OFFSETS = (
  74. ( 1, 0), ( 0, 1), (-1, 0), ( 0, -1), # 右下左上 (顺时针)
  75. ( 1, -1), ( 1, 1), (-1, 1), (-1, -1), # 四个对角
  76. )
  77. # ============== 主算法 ==============
  78. def find_path(map_data: MapData, begX: int, begY: int, endX: int, endY: int,
  79. dis_x: int = DEFAULT_DIS_X, dis_y: int = DEFAULT_DIS_Y,
  80. max_iter: int = 100000) -> list:
  81. """
  82. A* 寻路主入口 (与 PathFindController:FindPathAStar 行为一致)
  83. :param map_data: 来自 map_parser.load() 的地图
  84. :param begX, begY: 起点 (调用方: 角色当前位置)
  85. :param endX, endY: 终点 (调用方: 目标位置)
  86. :param dis_x, dis_y: 寻路范围限制 (与 Lua updateLimits 一致)
  87. :param max_iter: 最大迭代次数, 防止极端情况死循环
  88. :return: 路径点列表 [(x, y), ...] 从 start 到 end, 失败返回 []
  89. """
  90. if not _is_passable(map_data, begX, begY) or not _is_passable(map_data, endX, endY):
  91. return []
  92. if not _is_reachable(map_data, endX, endY):
  93. return [] # 终点是死格 (8 邻居都不可走)
  94. if (begX, begY) == (endX, endY):
  95. return [(begX, begY)]
  96. # 与 Lua updateLimits 一致: 限制在 (begX ± dis_x) × (begY ± dis_y) 的包围盒
  97. min_x = max(0, begX - dis_x)
  98. max_x = min(map_data.width - 1, begX + dis_x)
  99. min_y = max(0, begY - dis_y)
  100. max_y = min(map_data.height - 1, begY + dis_y)
  101. # 限制终点必须在包围盒内, 否则路径被截断 (与 Lua 行为一致)
  102. if not (min_x <= endX <= max_x and min_y <= endY <= max_y):
  103. return []
  104. # A* (标准实现, 二叉堆优先队列)
  105. start = PathNode(begX, begY, g=0, h=heuristic(begX, begY, endX, endY))
  106. open_heap: list = [] # (f, counter, node) 避免节点直接比较
  107. counter = 0
  108. heapq.heappush(open_heap, (start.f, counter, start))
  109. g_score: dict = {(begX, begY): 0}
  110. closed: set = set()
  111. iter_count = 0
  112. while open_heap and iter_count < max_iter:
  113. iter_count += 1
  114. _, _, current = heapq.heappop(open_heap)
  115. if (current.x, current.y) in closed:
  116. continue
  117. if (current.x, current.y) == (endX, endY):
  118. return _reconstruct_path(current)
  119. closed.add((current.x, current.y))
  120. cur_cell = map_data.get_cell(current.x, current.y)
  121. for dx, dy in NEIGHBOR_OFFSETS:
  122. nx, ny = current.x + dx, current.y + dy
  123. if not (min_x <= nx <= max_x and min_y <= ny <= max_y):
  124. continue
  125. n_cell = map_data.get_cell(nx, ny)
  126. if not n_cell.is_walkable:
  127. continue
  128. # 对角穿越检查 (任一正交邻居是障碍则不通过对角, 防"穿墙")
  129. if dx != 0 and dy != 0:
  130. side1 = map_data.get_cell(current.x + dx, current.y)
  131. side2 = map_data.get_cell(current.x, current.y + dy)
  132. if not side1.is_walkable or not side2.is_walkable:
  133. continue
  134. tentative_g = current.g + step_cost(cur_cell, n_cell, current.x, current.y, nx, ny)
  135. key = (nx, ny)
  136. if tentative_g < g_score.get(key, float("inf")):
  137. g_score[key] = tentative_g
  138. h = heuristic(nx, ny, endX, endY)
  139. f = tentative_g + h
  140. counter += 1
  141. heapq.heappush(open_heap, (f, counter, PathNode(nx, ny, tentative_g, h, current)))
  142. return [] # 未找到路径
  143. def _is_passable(map_data: MapData, x: int, y: int) -> bool:
  144. """格是否可走 (在地图内 且 无障碍)"""
  145. if not (0 <= x < map_data.width and 0 <= y < map_data.height):
  146. return False
  147. return map_data.get_cell(x, y).is_walkable
  148. def _is_reachable(map_data: MapData, x: int, y: int) -> bool:
  149. """
  150. 是否"非死格" (8 邻居至少一个可走)
  151. 与 Lua PathFindController:IsImpass 等价
  152. """
  153. if not _is_passable(map_data, x, y):
  154. return False
  155. for dx, dy in NEIGHBOR_OFFSETS:
  156. if _is_passable(map_data, x + dx, y + dy):
  157. return True
  158. return False
  159. def _reconstruct_path(end_node: PathNode) -> list:
  160. """回溯父指针, 输出从 start 到 end 的列表"""
  161. path = []
  162. node = end_node
  163. while node is not None:
  164. path.append((node.x, node.y))
  165. node = node.parent
  166. return path[::-1]
  167. # ============== 工具函数 ==============
  168. def path_cost(map_data: MapData, path: list) -> int:
  169. """计算路径总代价 (用于验证/调试)"""
  170. if len(path) < 2:
  171. return 0
  172. total = 0
  173. for (x1, y1), (x2, y2) in zip(path, path[1:]):
  174. c1 = map_data.get_cell(x1, y1)
  175. c2 = map_data.get_cell(x2, y2)
  176. total += int(step_cost(c1, c2, x1, y1, x2, y2))
  177. return total
  178. def path_is_valid(map_data: MapData, path: list) -> bool:
  179. """验证路径是否合法 (所有格可走 + 相邻格是 8 邻居之一)"""
  180. if not path:
  181. return False
  182. for x, y in path:
  183. if not _is_passable(map_data, x, y):
  184. return False
  185. for (x1, y1), (x2, y2) in zip(path, path[1:]):
  186. dx, dy = abs(x2 - x1), abs(y2 - y1)
  187. if dx > 1 or dy > 1 or (dx == 0 and dy == 0):
  188. return False
  189. return True
  190. def find_path_extended(map_data: MapData, begX: int, begY: int, endX: int, endY: int,
  191. chunk_size: int = 20) -> list:
  192. """
  193. 分段寻路: 解决 48×32 包围盒限制下的长距离寻路
  194. 思路: 沿直线每隔 chunk_size 格设 waypoint, 逐段寻路拼接
  195. :param chunk_size: 每段距离 (应 <= 2*dis_x)
  196. """
  197. full_path = []
  198. cur = (begX, begY)
  199. target = (endX, endY)
  200. # 计算直线 waypoint
  201. waypoints = _straight_waypoints(cur, target, chunk_size)
  202. waypoints.append(target)
  203. for wp in waypoints:
  204. seg = find_path(map_data, cur[0], cur[1], wp[0], wp[1])
  205. if not seg:
  206. return [] # 任何一段失败 → 整条失败
  207. if full_path and seg[0] == full_path[-1]:
  208. seg = seg[1:] # 去重
  209. full_path.extend(seg)
  210. cur = wp
  211. if cur == target:
  212. break
  213. return full_path
  214. def _straight_waypoints(start: tuple, end: tuple, step: int) -> list:
  215. """沿直线从 start 到 end 每 step 格放一个 waypoint (不含 end)"""
  216. dx = end[0] - start[0]
  217. dy = end[1] - start[1]
  218. dist = max(abs(dx), abs(dy))
  219. if dist <= step:
  220. return []
  221. n = dist // step
  222. if n <= 0:
  223. return []
  224. return [(start[0] + dx * i // n, start[1] + dy * i // n) for i in range(1, n + 1)]
  225. # ============== 自测 ==============
  226. if __name__ == "__main__":
  227. sample_dir = "/Users/zlwu/work/00code/传奇/996传奇盒子资源包/03-游戏客户端/jycq_206923/scene/map"
  228. from map_parser import load
  229. for name in ["0139.map", "3.map", "d022.map"]:
  230. path = os.path.join(sample_dir, name)
  231. m = load(path)
  232. # 找两个可走的点 (避免随机化, 选固定的)
  233. w, h = m.width, m.height
  234. # 选地图的 1/4 和 3/4 位置
  235. start_x, start_y = w // 4, h // 4
  236. end_x, end_y = 3 * w // 4, 3 * h // 4
  237. # 反复找, 直到遇到可走的
  238. while not _is_passable(m, start_x, start_y) and start_x < w - 1:
  239. start_x += 1
  240. while not _is_passable(m, end_x, end_y) and end_x > 0:
  241. end_x -= 1
  242. if not (_is_passable(m, start_x, start_y) and _is_passable(m, end_x, end_y)):
  243. print(f"=== {name}: 找不到可走端点, 跳过 ===")
  244. continue
  245. path_pts = find_path(m, start_x, start_y, end_x, end_y)
  246. print(f"=== {name} ({w}x{h}) ===")
  247. print(f" start=({start_x},{start_y}) end=({end_x},{end_y})")
  248. print(f" path: {len(path_pts)} 点, 代价={path_cost(m, path_pts)}")
  249. if path_pts:
  250. print(f" first 5: {path_pts[:5]}")
  251. print(f" last 5: {path_pts[-5:]}")
  252. print(f" valid: {path_is_valid(m, path_pts)}")
  253. print()