refactor(02-02): apply user review directives on path module

- 移除 system.ts 中 'mover' in movable 运行时形状检查与 IMovableWithMover,
  依 types.ts 修正后的类型直接绑定 IObjectMover<IObjectMovable>
- path 内导出类型(IPathGraph*、IPathfindingGraphBuilder)归位 types.ts,
  graph.ts/finder.ts 改由 types 导入
- 移除类与接口声明上的 jsdoc、类实现侧与接口重复的 jsdoc、region 标记;
  实现注释改为仅描述可观察行为(去除 Dijkstra 等实现叙述)
- 私有方法置于调用者之前(resolveFloorId、getNodeCost、search、startMove)
- system.test.ts 对齐 useMover(mover) 新签名并同步测试描述
This commit is contained in:
unanmed 2026-09-09 18:47:40 +08:00
parent 9a604422f8
commit d9ee80f77f
6 changed files with 165 additions and 264 deletions

View File

@ -7,16 +7,14 @@ import {
IStateBase
} from '@user/data-base';
import { isNil } from 'lodash-es';
import { IPathGraph, PathfindingGraphBuilder } from './graph';
import { IPathfinder, IPathfindingStep, PathCostFunction } from './types';
import { PathfindingGraphBuilder } from './graph';
import {
IPathfinder,
IPathfindingStep,
IPathGraph,
PathCostFunction
} from './types';
//#region 寻路求解器
/**
* 寻路求解器,在动态构建的有向图上执行最小损失搜索。
* 损失函数与通行性谓词均为可注入槽位,未注入损失函数时每格损失 1,
* 未注入谓词时所有边均不可通行
*/
export class PathfindingFinder implements IPathfinder {
/** 当前对象对应的数据层对象 */
readonly state: IStateBase;
@ -36,80 +34,43 @@ export class PathfindingFinder implements IPathfinder {
this.state = state;
}
/**
* 绑定寻路所用的地图状态对象
* @param maps 地图状态对象,传入 `null` 解绑
*/
useMapState(maps: IMapState | null): void {
this.maps = maps;
}
/**
* 绑定构建有向图所用的地图图层
* @param layer 地图图层对象,传入 `null` 解绑
*/
useMapLayer(layer: IMapLayer | null): void {
this.layer = layer;
}
/**
* 设置自定义损失函数
* @param cost 损失函数,传入 `null` 恢复默认
*/
useCostFunction(cost: PathCostFunction | null): void {
this.cost = cost;
}
/**
* 设置通行性谓词
* @param predicate 通行性谓词,传入 `null` 恢复默认
*/
usePassPredicate(predicate: IPassPredicate | null): void {
this.predicate = predicate;
}
/**
* 设置寻路使用的朝向组
* @param group 朝向组
*/
useDirGroup(group: number): void {
this.group = group;
}
/**
* 在当前绑定状态下执行寻路,返回损失最小的步骤序列。
* 地图状态或图层未绑定、坐标越界等非法输入下告警并返回空数组;
* 目标不可达时同样返回空数组
* @param start 寻路起始位置
* @param target 寻路目标位置
* 获取进入指定位置节点的损失,损失值非有限数或负数时
* 告警并按默认损失 1 处理
* @param block 位置信息
*/
find(start: ITileLocator, target: ITileLocator): IPathfindingStep[] {
const maps = this.maps;
const layer = this.layer;
if (isNil(maps) || isNil(layer)) {
logger.warn(173);
return [];
private getNodeCost(block: ILayerLocation): number {
if (!this.cost) return 1;
const value = this.cost(block);
if (!Number.isFinite(value) || value < 0) {
logger.warn(174);
return 1;
}
if (
!layer.inMap(start.x, start.y) ||
!layer.inMap(target.x, target.y)
) {
logger.warn(173);
return [];
}
// 数据端状态可变,每次寻路动态构建图,不做缓存
const builder = new PathfindingGraphBuilder();
builder.useMapState(maps);
builder.useMapLayer(layer);
builder.usePassPredicate(this.predicate);
builder.useDirGroup(this.group);
const graph = builder.build();
return this.search(graph, start, target);
return value;
}
/**
* 在有向图上执行 Dijkstra 最小损失搜索,
* 在有向图上执行最小损失搜索,
* 终端节点仅可作为路径终点,不可作为中间节点
* @param graph 寻路有向图
* @param start 寻路起始位置
@ -177,19 +138,31 @@ export class PathfindingFinder implements IPathfinder {
}
/**
* 获取进入指定位置节点的损失,非有限数或负数时
* 告警并按默认损失 1 处理,保证搜索的非负权不变式
* @param block 位置信息
* 地图状态或图层未绑定、坐标越界等非法输入下告警并返回空数组,
* 目标不可达时同样返回空数组
*/
private getNodeCost(block: ILayerLocation): number {
if (!this.cost) return 1;
const value = this.cost(block);
if (!Number.isFinite(value) || value < 0) {
logger.warn(174);
return 1;
find(start: ITileLocator, target: ITileLocator): IPathfindingStep[] {
const maps = this.maps;
const layer = this.layer;
if (isNil(maps) || isNil(layer)) {
logger.warn(173);
return [];
}
return value;
if (
!layer.inMap(start.x, start.y) ||
!layer.inMap(target.x, target.y)
) {
logger.warn(173);
return [];
}
// 数据端状态可变,每次寻路动态构建图,不做缓存
const builder = new PathfindingGraphBuilder();
builder.useMapState(maps);
builder.useMapLayer(layer);
builder.usePassPredicate(this.predicate);
builder.useDirGroup(this.group);
const graph = builder.build();
return this.search(graph, start, target);
}
}
//#endregion

View File

@ -61,7 +61,6 @@ beforeAll(async () => {
};
});
/** 测试图块定义,键为图块数字 */
interface TestTileDefinition {
/** 图块数字 */
num: number;

View File

@ -14,43 +14,12 @@ import {
IPassPredicate
} from '@user/data-base';
import { isNil } from 'lodash-es';
//#region 图结构
export interface IPathGraphEdge {
/** 本条边对应的移动方向 */
readonly dir: FaceDirection;
/** 边指向的节点索引,值为 y * width + x */
readonly to: number;
}
export interface IPathGraphNode {
/** 节点索引,值为 y * width + x */
readonly index: number;
/** 节点横坐标 */
readonly x: number;
/** 节点纵坐标 */
readonly y: number;
/** 节点对应的位置信息,用于损失计算 */
readonly block: ILayerLocation;
/** 该节点是否仅可作为路径终点,不可作为中间节点 */
readonly terminal: boolean;
/** 该节点的全部出边 */
readonly edges: readonly IPathGraphEdge[];
}
export interface IPathGraph {
/** 图宽度 */
readonly width: number;
/** 图高度 */
readonly height: number;
/** 图内全部节点,键为节点索引,值为 y * width + x */
readonly nodes: ReadonlyMap<number, IPathGraphNode>;
}
//#endregion
//#region 方向解析
import {
IPathGraph,
IPathGraphEdge,
IPathGraphNode,
IPathfindingGraphBuilder
} from './types';
/**
* 将方向描述器的坐标增量解析为对应的朝向
@ -69,51 +38,6 @@ function directionOf(x: number, y: number): FaceDirection {
return FaceDirection.Unknown;
}
//#endregion
//#region 有向图构建
/**
* 寻路有向图构建器接口,声明图构建的绑定槽位与构建入口
*/
export interface IPathfindingGraphBuilder {
/**
* 绑定地图状态对象,用于解析图层所属楼层 id
* @param maps 地图状态对象,传入 `null` 解绑
*/
useMapState(maps: IMapState | null): void;
/**
* 绑定构建有向图所用的地图图层
* @param layer 地图图层对象,传入 `null` 解绑
*/
useMapLayer(layer: IMapLayer | null): void;
/**
* 注入判定边可行性的通行性谓词
* @param predicate 通行性谓词,传入 `null` 解绑
*/
usePassPredicate(predicate: IPassPredicate | null): void;
/**
* 设置邻域使用的方向组
* @param group 朝向组
*/
useDirGroup(group: number): void;
/**
* 构建有向图。图层未绑定时告警并返回空图,
* 不包含任何节点与边
* @returns 构建的有向图
*/
build(): IPathGraph;
}
/**
* 寻路有向图构建器,将地图图层转换为以通行性谓词判定边的有向图。
* 邻域方向由注入的方向组决定,默认仅包含四正交方向;
* 谓词未注入时所有边均不可通行
*/
export class PathfindingGraphBuilder implements IPathfindingGraphBuilder {
/** 绑定的地图状态对象,用于解析图层所属楼层 id */
private maps: IMapState | null = null;
@ -143,6 +67,20 @@ export class PathfindingGraphBuilder implements IPathfindingGraphBuilder {
this.group = group;
}
/**
* 解析绑定图层所属的楼层 id
* @returns 楼层 id,无法解析时为 `undefined`
*/
private resolveFloorId(): string | undefined {
const maps = this.maps;
const layer = this.layer;
if (!maps || !layer) return undefined;
for (const [floorId, map] of maps.iterateAllMaps()) {
if (map === layer.map) return floorId;
}
return undefined;
}
build(): IPathGraph {
const layer = this.layer;
if (isNil(layer)) {
@ -220,20 +158,4 @@ export class PathfindingGraphBuilder implements IPathfindingGraphBuilder {
}
return { width, height, nodes };
}
/**
* 解析绑定图层所属的楼层 id
* @returns 楼层 id,无法解析时为 `undefined`
*/
private resolveFloorId(): string | undefined {
const maps = this.maps;
const layer = this.layer;
if (!maps || !layer) return undefined;
for (const [floorId, map] of maps.iterateAllMaps()) {
if (map === layer.map) return floorId;
}
return undefined;
}
}
//#endregion

View File

@ -68,7 +68,6 @@ beforeAll(async () => {
};
});
/** 测试图块定义,键为图块数字 */
interface TestTileDefinition {
/** 图块数字 */
num: number;
@ -111,7 +110,6 @@ const HIT_TILE: TestTileDefinition = {
const ALL_TILES: TestTileDefinition[] = [OPEN_TILE, WALL_TILE, HIT_TILE];
/** 测试用移动对象接口,记录 setPos 调用并携带移动器 */
interface TestTile extends IObjectMovable {
/** 当前横坐标 */
x: number;
@ -202,7 +200,6 @@ class FixturePredicate implements IPassPredicate {
}
}
/** 寻路系统测试夹具 */
interface SystemFixture {
/** 楼层地图对象 */
map: IGameMap;
@ -335,7 +332,7 @@ function createSystem(rows: number[], width: number): SystemFixture {
const layer = map.getLayerByAlias('event')!;
const system = new modules.PathfindingSystem(commonState as never);
const tile = createTestTile();
system.useMover(tile);
system.useMover(tile.mover);
system.finder.useMapState(maps);
system.finder.useMapLayer(layer);
return { map, layer, system, tile };
@ -474,15 +471,15 @@ describe('pathfinding system', () => {
expect(fixture.tile.y).toBe(0);
});
// 验证 moveTo 未绑定移动对象或无路径时返回 null 而非异常
it('returns null from moveTo when movable is unbound or unreachable', () => {
// 验证未绑定移动器或无路径时 moveTo 返回 null 而非异常
it('returns null from moveTo when mover is unbound or unreachable', () => {
const fixture = createSystem([1, 6, 1, 1, 6, 1, 1, 6, 1], 3);
injectPredicate(fixture);
fixture.system.useMover(null);
expect(fixture.system.moveTo({ x: 2, y: 1 })).toBeNull();
fixture.system.useMover(fixture.tile);
fixture.system.useMover(fixture.tile.mover);
expect(fixture.system.moveTo({ x: 2, y: 1 })).toBeNull();
});

View File

@ -11,39 +11,12 @@ import {
PathFallbackPolicy
} from './types';
//#region 移动器识别
/**
* 携带移动器的移动对象,动态图块等真实移动对象均满足此结构
*/
export interface IMovableWithMover extends IObjectMovable {
/** 移动器对象 */
readonly mover: IObjectMover<IObjectMovable>;
}
/**
* 判断移动对象是否携带移动器
* @param movable 移动对象
*/
function hasMover(movable: IObjectMovable): movable is IMovableWithMover {
return 'mover' in movable;
}
//#endregion
//#region 寻路系统
/**
* 寻路系统,绑定移动对象后提供仅取路径、逐步寻路与瞬移寻路入口。
* 瞬移前经回退策略判定,判定需要回退或未注入策略时
* 自动退为逐步寻路
*/
export class PathfindingSystem implements IPathfindingSystem {
/** 寻路求解器 */
readonly finder: IPathfinder;
/** 绑定的移动对象 */
private movable: IObjectMovable | null = null;
/** 绑定的移动器对象 */
private mover: IObjectMover<IObjectMovable> | null = null;
/** 注入的瞬移回退策略,未注入时必定回退为逐步寻路 */
private policy: PathFallbackPolicy | null = null;
/** 最近一次寻路移动的控制器包装 */
@ -53,77 +26,31 @@ export class PathfindingSystem implements IPathfindingSystem {
this.finder = new PathfindingFinder(state);
}
/**
* 绑定寻路移动对象,可绑定勇士位置或任意动态图块
* @param movable 移动对象,传入 `null` 解绑
*/
useMover(movable: IObjectMovable | null): void {
this.movable = movable;
useMover(mover: IObjectMover<IObjectMovable> | null): void {
this.mover = mover;
}
/**
* 设置瞬移回退策略,传入 `null` 恢复默认必定逐步
* @param policy 回退策略函数
*/
useFallbackPolicy(policy: PathFallbackPolicy | null): void {
this.policy = policy;
}
/**
* 仅获取从当前位置至目标位置的最小损失路径,不产生任何移动。
* 未绑定移动对象时告警并返回空数组
* 未绑定移动器时告警并返回空数组
* @param target 目标坐标
*/
getPath(target: ITileLocator): IPathfindingStep[] {
const movable = this.movable;
if (isNil(movable)) {
const mover = this.mover;
if (isNil(mover)) {
logger.warn(173);
return [];
}
return this.finder.find({ x: movable.x, y: movable.y }, target);
}
/**
* 逐步寻路至目标位置,触发途经事件。
* 无法寻路、无路径或已有移动进行中时返回 `null`
* @param target 目标坐标
*/
moveTo(target: ITileLocator): IPathfindingController | null {
const path = this.getPath(target);
if (path.length === 0) return null;
return this.startMove(path, false);
}
/**
* 瞬移至目标位置。瞬移前经回退策略判定,
* 判定需要回退则自动退为逐步寻路;
* 无法寻路、无路径或已有移动进行中时返回 `null`
* @param target 目标坐标
*/
teleportTo(target: ITileLocator): IPathfindingController | null {
const path = this.getPath(target);
if (path.length === 0) return null;
if (isNil(this.policy) || this.policy(path)) {
return this.startMove(path, false);
}
return this.startMove(path, true);
}
/**
* 打断当前自动寻路。新的方向输入或新的寻路调用可随时打断并接管,
* 接管时序由 03 计划的接线定义,此处仅停止进行中的移动
*/
async interrupt(): Promise<void> {
const current = this.current;
this.current = null;
if (current && !current.controller.done) {
await current.controller.stop();
}
const tile = mover.tile;
return this.finder.find({ x: tile.x, y: tile.y }, target);
}
/**
* 按指定移动方式启动寻路移动,返回控制器包装。
* 对象未携带移动器或已有移动进行中时返回 `null`
* 已有移动进行中时返回 `null`
* @param path 寻路步骤序列
* @param teleport 是否瞬移
*/
@ -131,12 +58,10 @@ export class PathfindingSystem implements IPathfindingSystem {
path: readonly IPathfindingStep[],
teleport: boolean
): IPathfindingController | null {
const movable = this.movable;
if (!movable) return null;
const mover = this.mover;
if (!mover) return null;
const current = this.current;
if (current && !current.controller.done) return null;
if (!hasMover(movable)) return null;
const mover = movable.mover;
if (teleport) {
const last = path[path.length - 1];
@ -154,6 +79,27 @@ export class PathfindingSystem implements IPathfindingSystem {
this.current = result;
return result;
}
}
//#endregion
moveTo(target: ITileLocator): IPathfindingController | null {
const path = this.getPath(target);
if (path.length === 0) return null;
return this.startMove(path, false);
}
teleportTo(target: ITileLocator): IPathfindingController | null {
const path = this.getPath(target);
if (path.length === 0) return null;
if (isNil(this.policy) || this.policy(path)) {
return this.startMove(path, false);
}
return this.startMove(path, true);
}
async interrupt(): Promise<void> {
const current = this.current;
this.current = null;
if (current && !current.controller.done) {
await current.controller.stop();
}
}
}

View File

@ -121,3 +121,67 @@ export interface IPathfindingSystem extends IDataBaseExtended {
*/
interrupt(): Promise<void>;
}
export interface IPathGraphEdge {
/** 本条边对应的移动方向 */
readonly dir: FaceDirection;
/** 边指向的节点索引,值为 y * width + x */
readonly to: number;
}
export interface IPathGraphNode {
/** 节点索引,值为 y * width + x */
readonly index: number;
/** 节点横坐标 */
readonly x: number;
/** 节点纵坐标 */
readonly y: number;
/** 节点对应的位置信息,用于损失计算 */
readonly block: ILayerLocation;
/** 该节点是否仅可作为路径终点,不可作为中间节点 */
readonly terminal: boolean;
/** 该节点的全部出边 */
readonly edges: readonly IPathGraphEdge[];
}
export interface IPathGraph {
/** 图宽度 */
readonly width: number;
/** 图高度 */
readonly height: number;
/** 图内全部节点,键为节点索引 */
readonly nodes: ReadonlyMap<number, IPathGraphNode>;
}
export interface IPathfindingGraphBuilder {
/**
* 绑定地图状态对象,用于解析图层所属楼层 id
* @param maps 地图状态对象,传入 `null` 解绑
*/
useMapState(maps: IMapState | null): void;
/**
* 绑定构建有向图所用的地图图层
* @param layer 地图图层对象,传入 `null` 解绑
*/
useMapLayer(layer: IMapLayer | null): void;
/**
* 注入判定边可行性的通行性谓词
* @param predicate 通行性谓词,传入 `null` 解绑
*/
usePassPredicate(predicate: IPassPredicate | null): void;
/**
* 设置邻域使用的方向组
* @param group 朝向组
*/
useDirGroup(group: number): void;
/**
* 构建有向图。图层未绑定时告警并返回空图,
* 不包含任何节点与边
* @returns 构建的有向图
*/
build(): IPathGraph;
}