template/packages-user/data-system/src/path/performance.test.ts
unanmed 489e3de106 test(02-02): add full-pipeline pathfinding performance test and record review-fix round
- 新增 performance.test.ts:100x100 固定种子障碍图上计时完整 find 流水线
  (建图 + 最优路径发现),结构化输出 map/steps/elapsed
- 实测 map=100x100 steps=198 elapsed≈609ms(sanity 上限 5000ms)
- 02-02-SUMMARY 追加 Review Fixes (user audit round 1) 记录
2026-09-09 18:51:31 +08:00

295 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 测试寻路性能:从有向图构建到最优路径发现的完整 find 流水线总耗时
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { type ITileLocator } from '@motajs/common';
import { FaceDirection } from '@user/data-common';
import {
type IDataCommon,
type IFaceHandler,
type IObjectMovable,
type IObjectMover,
type ITileStore
} from '@user/data-common';
import {
type IGameMap,
type IPassCheckHandler,
type IPassPredicate
} from '@user/data-base';
import { type PathfindingSystem } from './system';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
Map.prototype.getOrInsertComputed ??= function <K, V>(
this: Map<K, V>,
key: K,
callback: (key: K) => V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
const value = callback(key);
this.set(key, value);
return value;
};
});
interface TestModules {
PathfindingSystem: typeof import('./system').PathfindingSystem;
MapState: typeof import('@user/data-base').MapState;
TileStore: typeof import('@user/data-common').TileStore;
ObjectMover: typeof import('@user/data-common').ObjectMover;
FaceManager: typeof import('@user/data-common').FaceManager;
Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler;
RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const systemModule = await import('./system');
const baseModule = await import('@user/data-base');
const commonModule = await import('@user/data-common');
modules = {
PathfindingSystem: systemModule.PathfindingSystem,
MapState: baseModule.MapState,
TileStore: commonModule.TileStore,
ObjectMover: commonModule.ObjectMover,
FaceManager: commonModule.FaceManager,
Dir8FaceHandler: commonModule.Dir8FaceHandler,
RoleFaceBinder: commonModule.RoleFaceBinder
};
});
/** 性能地图边长,共 100 x 100 = 10000 个节点 */
const MAP_SIZE = 100;
/** 性能测试的宽松耗时上限,单位 ms仅作 sanity 把关 */
const ELAPSED_LIMIT_MS = 5000;
interface TestTile extends IObjectMovable {
x: number;
y: number;
face: FaceDirection;
mover: IObjectMover<TestTile>;
}
/**
* 复刻 DefaultHeroMoveTopImpl 掩码语义的测试谓词:
* 事件层恒参与判定,四角朝向直接放行
*/
class FixturePredicate implements IPassPredicate {
/** 绑定的楼层地图对象 */
private readonly map: IGameMap;
/** 朝向管理对象,用于求相反方向 */
private readonly face: IFaceHandler<FaceDirection>;
constructor(map: IGameMap, face: IFaceHandler<FaceDirection>) {
this.map = map;
this.face = face;
}
/**
* 将朝向转换为对应的通行性位掩码
* @param dir 朝向
*/
private passBit(dir: FaceDirection): number {
switch (dir) {
case FaceDirection.Up:
return 0b0001;
case FaceDirection.Right:
return 0b0010;
case FaceDirection.Down:
return 0b0100;
case FaceDirection.Left:
return 0b1000;
default:
return 0;
}
}
canPass(handler: IPassCheckHandler): boolean {
const event = this.map.eventLayer;
if (!event) return false;
const { currLoc, nextLoc, direction } = handler;
// 四角朝向直接判定为可通行,与 moverImpl 语义一致
if (
direction === FaceDirection.LeftDown ||
direction === FaceDirection.LeftUp ||
direction === FaceDirection.RightDown ||
direction === FaceDirection.RightUp
) {
return true;
}
const opposite = this.face.opposite(direction);
const leaveMask = this.passBit(direction);
const enterMask = this.passBit(opposite);
let canLeave = true;
let canEnter = true;
const curr = event.getLocationData(currLoc.x, currLoc.y);
const next = event.getLocationData(nextLoc.x, nextLoc.y);
const currRaw = curr?.static.raw();
const nextRaw = next?.static.raw();
if (currRaw) {
canLeave = !!(leaveMask & currRaw.pass.outPass);
}
if (nextRaw) {
canEnter = !!(enterMask & nextRaw.pass.inPass);
}
return canLeave && canEnter;
}
shouldHit(): boolean {
return false;
}
}
/**
* 以固定种子生成含约两成墙体的确定性地图数据,
* 起点与终点强制为开阔图块
* @param size 地图边长
*/
function generateRows(size: number): number[] {
let seed = 20260909;
const next = () => {
seed = (seed * 1664525 + 1013904223) % 4294967296;
return seed / 4294967296;
};
const rows: number[] = [];
for (let i = 0; i < size * size; i++) {
rows.push(next() < 0.2 ? 6 : 1);
}
rows[0] = 1;
rows[size * size - 1] = 1;
return rows;
}
/**
* 创建绑定 100 x 100 地图与移动对象的寻路系统夹具
* @param rows 每行图块数字,行长为宽度乘高度
* @param width 地图宽度
*/
function createPerformanceSystem(rows: number[], width: number) {
const tileStore: ITileStore = new modules.TileStore() as never;
const tileDefs = [
{ num: 1, id: 'open', outPass: 15, inPass: 15 },
{ num: 6, id: 'wall', outPass: 0, inPass: 0 }
];
for (const tile of tileDefs) {
tileStore.addTile({
num: tile.num,
id: tile.id,
events: {},
type: 0,
pass: {
onlyEvents: false,
outPass: tile.outPass,
inPass: tile.inPass
},
eventPass: true
});
}
const faceManager = new modules.FaceManager();
faceManager.register(1, new modules.Dir8FaceHandler());
const commonState: IDataCommon = {
tileStore,
itemStore: {},
mapStore: {},
eventStore: {},
roleFace: new modules.RoleFaceBinder(),
faceManager,
saveSystem: {}
} as never;
const maps = new modules.MapState(tileStore, commonState);
const map = maps.fromRaw({
floorId: 'PERF',
width,
map: { 0: rows },
layerAlias: { 0: 'event' },
events: { 0: {} }
})!;
const layer = map.getLayerByAlias('event')!;
const predicate = new FixturePredicate(map, new modules.Dir8FaceHandler());
class PerfMover
extends modules.ObjectMover<TestTile>
implements IObjectMover<TestTile>
{
readonly tile: TestTile;
constructor(tile: TestTile) {
super(new modules.Dir8FaceHandler(), FaceDirection.Down);
this.tile = tile;
}
protected override async onMoveStart(): Promise<void> {}
protected override async onMoveEnd(): Promise<void> {}
protected override async onStepStart(): Promise<number> {
return 0;
}
protected override async onStepEnd(): Promise<ITileLocator> {
return { x: this.tile.x, y: this.tile.y };
}
protected override async onStepSettled(): Promise<void> {}
}
class PerfTile implements TestTile {
x: number = 0;
y: number = 0;
face: FaceDirection = FaceDirection.Down;
mover: IObjectMover<TestTile>;
constructor() {
this.mover = new PerfMover(this);
}
setPos(x: number, y: number): void {
this.x = x;
this.y = y;
}
getCurrentFaceDirection(): FaceDirection {
return this.face;
}
}
const tile = new PerfTile();
const system: PathfindingSystem = new modules.PathfindingSystem(
commonState as never
);
system.useMover(tile.mover);
system.finder.useMapState(maps);
system.finder.useMapLayer(layer);
system.finder.usePassPredicate(predicate);
return system;
}
describe('pathfinding performance', () => {
// 验证 100 x 100 障碍地图上从建图到最优路径发现的完整寻路耗时处于可用量级
it('completes the full find pipeline on a 100x100 map within the sanity bound', () => {
const rows = generateRows(MAP_SIZE);
const system = createPerformanceSystem(rows, MAP_SIZE);
const start = performance.now();
const steps = system.getPath({ x: MAP_SIZE - 1, y: MAP_SIZE - 1 });
const elapsed = performance.now() - start;
// 审查要求输出结构化性能数据供汇报使用
// eslint-disable-next-line no-console
console.log(
`[pathfinding-perf] map=${MAP_SIZE}x${MAP_SIZE} ` +
`steps=${steps.length} elapsed=${elapsed.toFixed(2)}ms`
);
expect(steps.length).toBeGreaterThan(0);
expect(elapsed).toBeLessThan(ELAPSED_LIMIT_MS);
});
});