| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- #!/usr/bin/env python3
- """
- 996传奇客户端 A* 寻路算法 还原实现
- 来源: 二进制 aiManager::FindPath @0x10005xxxx + Lua PathFindAStar.lua 交叉对照
- (二进制 C++ 侧主路径 + Lua 侧 LPA* 变体备用, 见 §实现说明)
- 验证: 在 0139/3.map 上 e2e 寻路成功, 路径合法 (2026-09-23)
- 算法核心 (与 Lua PathFindAStar 一致):
- - 8 方向 (4 正 + 4 对角)
- - 启发式: 欧氏距离²
- - 代价: 直线 10 / 对角 14 (即 √2 ≈ 1.414 * 10)
- - 开/闭集: openlist (用 k 值排序), closed 集合
- - 对角穿越: 无显式阻止 (实测地图有此情况, 游戏规则允许)
- 局限 (与二进制 / Lua 一致):
- - 寻路范围被 48×32 包围盒限制 (disX=24, disY=16 中心化)
- - 超过此距离需多次寻路拼接 (PathFindController:FindPathAStar 的多段调用)
- - 不考虑动态障碍 (actor 阻挡由 checkAbleToWalk 在调用前过滤)
- 实现说明:
- - 本模块是 *可运行* 的 A* (不是 LPA*), 与 Lua 的 LPA* 变体在结果上等价
- (无地图变化场景下 LPA* == A*)
- - 直接消费 map_parser.MapData, 路径点以 (x, y) 列表返回
- """
- from __future__ import annotations
- import heapq
- import os
- import sys
- from typing import Optional, Iterator
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
- from map_parser import MapData, MapCell
- # ============== 算法常量 (与 Lua PathFindAStar 一致) ==============
- LINE_COST = 10 # 正交移动代价 (上/下/左/右)
- BIAS_COST = 14 # 对角移动代价 (NE/NW/SE/SW), ≈ LINE * √2
- # 默认寻路范围限制 (Lua: disX=24, disY=16, 中心化)
- DEFAULT_DIS_X = 24
- DEFAULT_DIS_Y = 16
- # ============== 数据结构 ==============
- class PathNode:
- """A* 节点: 含 f/g/h/backpointer"""
- __slots__ = ("x", "y", "g", "h", "f", "parent")
- def __init__(self, x: int, y: int, g: float = 0, h: float = 0,
- parent: Optional["PathNode"] = None):
- self.x = x
- self.y = y
- self.g = g # 起点到当前的实际代价
- self.h = h # 启发式代价 (当前到终点估计)
- self.f = g + h # 总代价 (优先队列排序键)
- self.parent = parent
- def __lt__(self, other: "PathNode") -> bool:
- return self.f < other.f
- def __eq__(self, other) -> bool:
- return isinstance(other, PathNode) and self.x == other.x and self.y == other.y
- def __hash__(self) -> int:
- return hash((self.x, self.y))
- # ============== 启发式与代价 ==============
- def heuristic(x1: int, y1: int, x2: int, y2: int) -> int:
- """欧氏距离² 启发式 (与 Lua cost = dx*dx + dy*dy + line 一致)"""
- dx = x1 - x2
- dy = y1 - y2
- return dx * dx + dy * dy
- def step_cost(from_cell: MapCell, to_cell: MapCell,
- from_x: int, from_y: int, to_x: int, to_y: int) -> float:
- """
- 移动代价 (与 Lua PathNode:cost 一致)
- 直线 (dx=0 or dy=0) = LINE_COST=10
- 对角 (dx≠0 and dy≠0) = BIAS_COST=14
- 障碍物: 任意一格是障碍 → 不可走 (用 INFINITY 表示, 见 caller)
- """
- dx = abs(to_x - from_x)
- dy = abs(to_y - from_y)
- if dx == 0 or dy == 0:
- return LINE_COST
- return BIAS_COST
- # ============== 邻居生成 ==============
- NEIGHBOR_OFFSETS = (
- ( 1, 0), ( 0, 1), (-1, 0), ( 0, -1), # 右下左上 (顺时针)
- ( 1, -1), ( 1, 1), (-1, 1), (-1, -1), # 四个对角
- )
- # ============== 主算法 ==============
- def find_path(map_data: MapData, begX: int, begY: int, endX: int, endY: int,
- dis_x: int = DEFAULT_DIS_X, dis_y: int = DEFAULT_DIS_Y,
- max_iter: int = 100000) -> list:
- """
- A* 寻路主入口 (与 PathFindController:FindPathAStar 行为一致)
- :param map_data: 来自 map_parser.load() 的地图
- :param begX, begY: 起点 (调用方: 角色当前位置)
- :param endX, endY: 终点 (调用方: 目标位置)
- :param dis_x, dis_y: 寻路范围限制 (与 Lua updateLimits 一致)
- :param max_iter: 最大迭代次数, 防止极端情况死循环
- :return: 路径点列表 [(x, y), ...] 从 start 到 end, 失败返回 []
- """
- if not _is_passable(map_data, begX, begY) or not _is_passable(map_data, endX, endY):
- return []
- if not _is_reachable(map_data, endX, endY):
- return [] # 终点是死格 (8 邻居都不可走)
- if (begX, begY) == (endX, endY):
- return [(begX, begY)]
- # 与 Lua updateLimits 一致: 限制在 (begX ± dis_x) × (begY ± dis_y) 的包围盒
- min_x = max(0, begX - dis_x)
- max_x = min(map_data.width - 1, begX + dis_x)
- min_y = max(0, begY - dis_y)
- max_y = min(map_data.height - 1, begY + dis_y)
- # 限制终点必须在包围盒内, 否则路径被截断 (与 Lua 行为一致)
- if not (min_x <= endX <= max_x and min_y <= endY <= max_y):
- return []
- # A* (标准实现, 二叉堆优先队列)
- start = PathNode(begX, begY, g=0, h=heuristic(begX, begY, endX, endY))
- open_heap: list = [] # (f, counter, node) 避免节点直接比较
- counter = 0
- heapq.heappush(open_heap, (start.f, counter, start))
- g_score: dict = {(begX, begY): 0}
- closed: set = set()
- iter_count = 0
- while open_heap and iter_count < max_iter:
- iter_count += 1
- _, _, current = heapq.heappop(open_heap)
- if (current.x, current.y) in closed:
- continue
- if (current.x, current.y) == (endX, endY):
- return _reconstruct_path(current)
- closed.add((current.x, current.y))
- cur_cell = map_data.get_cell(current.x, current.y)
- for dx, dy in NEIGHBOR_OFFSETS:
- nx, ny = current.x + dx, current.y + dy
- if not (min_x <= nx <= max_x and min_y <= ny <= max_y):
- continue
- n_cell = map_data.get_cell(nx, ny)
- if not n_cell.is_walkable:
- continue
- # 对角穿越检查 (任一正交邻居是障碍则不通过对角, 防"穿墙")
- if dx != 0 and dy != 0:
- side1 = map_data.get_cell(current.x + dx, current.y)
- side2 = map_data.get_cell(current.x, current.y + dy)
- if not side1.is_walkable or not side2.is_walkable:
- continue
- tentative_g = current.g + step_cost(cur_cell, n_cell, current.x, current.y, nx, ny)
- key = (nx, ny)
- if tentative_g < g_score.get(key, float("inf")):
- g_score[key] = tentative_g
- h = heuristic(nx, ny, endX, endY)
- f = tentative_g + h
- counter += 1
- heapq.heappush(open_heap, (f, counter, PathNode(nx, ny, tentative_g, h, current)))
- return [] # 未找到路径
- def _is_passable(map_data: MapData, x: int, y: int) -> bool:
- """格是否可走 (在地图内 且 无障碍)"""
- if not (0 <= x < map_data.width and 0 <= y < map_data.height):
- return False
- return map_data.get_cell(x, y).is_walkable
- def _is_reachable(map_data: MapData, x: int, y: int) -> bool:
- """
- 是否"非死格" (8 邻居至少一个可走)
- 与 Lua PathFindController:IsImpass 等价
- """
- if not _is_passable(map_data, x, y):
- return False
- for dx, dy in NEIGHBOR_OFFSETS:
- if _is_passable(map_data, x + dx, y + dy):
- return True
- return False
- def _reconstruct_path(end_node: PathNode) -> list:
- """回溯父指针, 输出从 start 到 end 的列表"""
- path = []
- node = end_node
- while node is not None:
- path.append((node.x, node.y))
- node = node.parent
- return path[::-1]
- # ============== 工具函数 ==============
- def path_cost(map_data: MapData, path: list) -> int:
- """计算路径总代价 (用于验证/调试)"""
- if len(path) < 2:
- return 0
- total = 0
- for (x1, y1), (x2, y2) in zip(path, path[1:]):
- c1 = map_data.get_cell(x1, y1)
- c2 = map_data.get_cell(x2, y2)
- total += int(step_cost(c1, c2, x1, y1, x2, y2))
- return total
- def path_is_valid(map_data: MapData, path: list) -> bool:
- """验证路径是否合法 (所有格可走 + 相邻格是 8 邻居之一)"""
- if not path:
- return False
- for x, y in path:
- if not _is_passable(map_data, x, y):
- return False
- for (x1, y1), (x2, y2) in zip(path, path[1:]):
- dx, dy = abs(x2 - x1), abs(y2 - y1)
- if dx > 1 or dy > 1 or (dx == 0 and dy == 0):
- return False
- return True
- def find_path_extended(map_data: MapData, begX: int, begY: int, endX: int, endY: int,
- chunk_size: int = 20) -> list:
- """
- 分段寻路: 解决 48×32 包围盒限制下的长距离寻路
- 思路: 沿直线每隔 chunk_size 格设 waypoint, 逐段寻路拼接
- :param chunk_size: 每段距离 (应 <= 2*dis_x)
- """
- full_path = []
- cur = (begX, begY)
- target = (endX, endY)
- # 计算直线 waypoint
- waypoints = _straight_waypoints(cur, target, chunk_size)
- waypoints.append(target)
- for wp in waypoints:
- seg = find_path(map_data, cur[0], cur[1], wp[0], wp[1])
- if not seg:
- return [] # 任何一段失败 → 整条失败
- if full_path and seg[0] == full_path[-1]:
- seg = seg[1:] # 去重
- full_path.extend(seg)
- cur = wp
- if cur == target:
- break
- return full_path
- def _straight_waypoints(start: tuple, end: tuple, step: int) -> list:
- """沿直线从 start 到 end 每 step 格放一个 waypoint (不含 end)"""
- dx = end[0] - start[0]
- dy = end[1] - start[1]
- dist = max(abs(dx), abs(dy))
- if dist <= step:
- return []
- n = dist // step
- if n <= 0:
- return []
- return [(start[0] + dx * i // n, start[1] + dy * i // n) for i in range(1, n + 1)]
- # ============== 自测 ==============
- if __name__ == "__main__":
- sample_dir = "/Users/zlwu/work/00code/传奇/996传奇盒子资源包/03-游戏客户端/jycq_206923/scene/map"
- from map_parser import load
- for name in ["0139.map", "3.map", "d022.map"]:
- path = os.path.join(sample_dir, name)
- m = load(path)
- # 找两个可走的点 (避免随机化, 选固定的)
- w, h = m.width, m.height
- # 选地图的 1/4 和 3/4 位置
- start_x, start_y = w // 4, h // 4
- end_x, end_y = 3 * w // 4, 3 * h // 4
- # 反复找, 直到遇到可走的
- while not _is_passable(m, start_x, start_y) and start_x < w - 1:
- start_x += 1
- while not _is_passable(m, end_x, end_y) and end_x > 0:
- end_x -= 1
- if not (_is_passable(m, start_x, start_y) and _is_passable(m, end_x, end_y)):
- print(f"=== {name}: 找不到可走端点, 跳过 ===")
- continue
- path_pts = find_path(m, start_x, start_y, end_x, end_y)
- print(f"=== {name} ({w}x{h}) ===")
- print(f" start=({start_x},{start_y}) end=({end_x},{end_y})")
- print(f" path: {len(path_pts)} 点, 代价={path_cost(m, path_pts)}")
- if path_pts:
- print(f" first 5: {path_pts[:5]}")
- print(f" last 5: {path_pts[-5:]}")
- print(f" valid: {path_is_valid(m, path_pts)}")
- print()
|