mirror of
https://github.com/motajs/template.git
synced 2026-09-13 10:18:50 +08:00
test(02-02): add real 13x13 game maps to pathfinding performance tests
- 夹具 createPerformanceSystem 参数化起始位置与图块定义(合成矩阵默认值不变) - 真实地图图块语义:数字 1 墙体,0/2/3/4/5/6 均为空地 - 新增六张 13x13 真实地图分段测量用例(20 轮 build/find/search,入口不足时固定种子抽选端点并记录)
This commit is contained in:
parent
f9c4f3b44a
commit
f1a9e626b6
@ -68,6 +68,12 @@ const MAP_SIZES = [10, 30, 60, 100, 150];
|
||||
/** 分段测量重复运行的次数 */
|
||||
const RUNS = 5;
|
||||
|
||||
/** 真实地图分段测量重复运行的次数,13x13 图极小需更多样本稳定均值 */
|
||||
const REAL_RUNS = 20;
|
||||
|
||||
/** 真实地图端点伪随机抽选的固定种子 */
|
||||
const REALMAP_SEED = 20260909;
|
||||
|
||||
interface MeasureResult<T> {
|
||||
/** 被测函数的返回值 */
|
||||
value: T;
|
||||
@ -218,18 +224,49 @@ function generateRows(size: number): number[] {
|
||||
return rows;
|
||||
}
|
||||
|
||||
interface TestTileDef {
|
||||
/** 图块数字 */
|
||||
num: number;
|
||||
/** 图块 id 别名 */
|
||||
id: string;
|
||||
/** 离开该图块的通行掩码 */
|
||||
outPass: number;
|
||||
/** 进入该图块的通行掩码 */
|
||||
inPass: number;
|
||||
}
|
||||
|
||||
/** 合成尺寸矩阵夹具的图块定义:数字 1 为开阔空地,数字 6 为墙体 */
|
||||
const SYNTHETIC_TILE_DEFS: TestTileDef[] = [
|
||||
{ num: 1, id: 'open', outPass: 15, inPass: 15 },
|
||||
{ num: 6, id: 'wall', outPass: 0, inPass: 0 }
|
||||
];
|
||||
|
||||
/** 真实地图夹具的图块定义:数字 1 为墙体,其余数字(0/2/3/4/5/6)均视为开阔空地 */
|
||||
const REALMAP_TILE_DEFS: TestTileDef[] = [
|
||||
{ num: 1, id: 'wall', outPass: 0, inPass: 0 },
|
||||
{ num: 0, id: 'open0', outPass: 15, inPass: 15 },
|
||||
{ num: 2, id: 'open2', outPass: 15, inPass: 15 },
|
||||
{ num: 3, id: 'open3', outPass: 15, inPass: 15 },
|
||||
{ num: 4, id: 'open4', outPass: 15, inPass: 15 },
|
||||
{ num: 5, id: 'open5', outPass: 15, inPass: 15 },
|
||||
{ num: 6, id: 'open6', outPass: 15, inPass: 15 }
|
||||
];
|
||||
|
||||
/**
|
||||
* 创建绑定指定尺寸地图与移动对象的寻路系统夹具
|
||||
* @param rows 每行图块数字,行长为宽度乘高度
|
||||
* @param width 地图宽度
|
||||
* @param start 移动器起始位置,默认地图左上角
|
||||
* @param defs 图块定义,默认为合成矩阵的开阔/墙体语义
|
||||
*/
|
||||
function createPerformanceSystem(rows: number[], width: number) {
|
||||
function createPerformanceSystem(
|
||||
rows: number[],
|
||||
width: number,
|
||||
start: ITileLocator = { x: 0, y: 0 },
|
||||
defs: TestTileDef[] = SYNTHETIC_TILE_DEFS
|
||||
) {
|
||||
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) {
|
||||
for (const tile of defs) {
|
||||
tileStore.addTile({
|
||||
num: tile.num,
|
||||
id: tile.id,
|
||||
@ -312,6 +349,7 @@ function createPerformanceSystem(rows: number[], width: number) {
|
||||
}
|
||||
|
||||
const tile = new PerfTile();
|
||||
tile.setPos(start.x, start.y);
|
||||
const system: PathfindingSystem = new modules.PathfindingSystem(
|
||||
commonState as never
|
||||
);
|
||||
@ -326,11 +364,172 @@ function createPerformanceSystem(rows: number[], width: number) {
|
||||
builder.useMapState(maps);
|
||||
builder.useMapLayer(layer);
|
||||
builder.usePassPredicate(predicate);
|
||||
return builder.build({ x: 0, y: 0 });
|
||||
return builder.build(start);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface IRealMapEndpoints {
|
||||
/** 寻路起始位置 */
|
||||
start: ITileLocator;
|
||||
/** 寻路目标位置 */
|
||||
target: ITileLocator;
|
||||
/** 是否存在伪随机抽选的端点,需在输出中记录所选图块 */
|
||||
seeded: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析真实地图的寻路端点:优先取按行序先后扫到的两个入口(数字 5),
|
||||
* 入口不足两个时以固定种子依次抽取非墙空地补足端点,保证结果可复现
|
||||
* @param map 真实地图的二维行
|
||||
* @param size 地图边长
|
||||
*/
|
||||
function resolveEndpoints(map: number[][], size: number): IRealMapEndpoints {
|
||||
const entrances: ITileLocator[] = [];
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
if (map[y]![x] === 5) entrances.push({ x, y });
|
||||
}
|
||||
}
|
||||
if (entrances.length >= 2) {
|
||||
return {
|
||||
start: entrances[0]!,
|
||||
target: entrances[1]!,
|
||||
seeded: false
|
||||
};
|
||||
}
|
||||
|
||||
let seed = REALMAP_SEED;
|
||||
const next = () => {
|
||||
seed = (seed * 1664525 + 1013904223) % 4294967296;
|
||||
return seed / 4294967296;
|
||||
};
|
||||
const pick = (): ITileLocator => {
|
||||
while (true) {
|
||||
const index = Math.floor(next() * size * size);
|
||||
const x = index % size;
|
||||
const y = Math.floor(index / size);
|
||||
if (map[y]![x] !== 1) return { x, y };
|
||||
}
|
||||
};
|
||||
const start = entrances[0] ?? pick();
|
||||
let target = pick();
|
||||
while (target.x === start.x && target.y === start.y) {
|
||||
target = pick();
|
||||
}
|
||||
return { start, target, seeded: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一组耗时的最小值与平均值格式化为 min/avg 形式,单位 ms
|
||||
* @param times 耗时列表,单位 ms
|
||||
*/
|
||||
function minAvg(times: number[]): string {
|
||||
const min = Math.min(...times);
|
||||
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
return `${min.toFixed(2)}/${avg.toFixed(2)}ms`;
|
||||
}
|
||||
|
||||
/** 用户提供的六张 13x13 真实游戏地图:1 为墙体,5 为入口,其余数字均为空地 */
|
||||
const REAL_MAPS: number[][][] = [
|
||||
// 地图 1:无入口,需伪随机抽取空地端点
|
||||
[
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 4, 2, 0, 3, 1, 5, 2, 0, 3, 3, 3, 1],
|
||||
[1, 3, 1, 3, 0, 2, 4, 1, 4, 1, 1, 2, 1],
|
||||
[1, 1, 1, 1, 4, 1, 1, 1, 3, 0, 4, 0, 1],
|
||||
[1, 0, 4, 3, 0, 4, 3, 0, 4, 1, 1, 3, 1],
|
||||
[1, 4, 1, 1, 1, 1, 4, 1, 0, 3, 1, 1, 1],
|
||||
[1, 0, 3, 1, 3, 1, 3, 1, 1, 4, 1, 3, 1],
|
||||
[1, 3, 0, 4, 0, 4, 0, 3, 1, 0, 2, 0, 1],
|
||||
[1, 1, 4, 1, 1, 1, 2, 1, 1, 2, 1, 4, 1],
|
||||
[1, 3, 0, 4, 3, 4, 0, 0, 4, 0, 4, 0, 1],
|
||||
[1, 4, 1, 1, 1, 1, 2, 1, 1, 1, 1, 4, 1],
|
||||
[1, 3, 1, 3, 3, 3, 0, 0, 3, 3, 1, 3, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
// 地图 2:入口 (12,11) 与 (1,12)
|
||||
[
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1],
|
||||
[1, 0, 4, 0, 2, 0, 2, 0, 2, 0, 4, 0, 1],
|
||||
[1, 1, 1, 4, 1, 4, 1, 4, 1, 4, 1, 1, 1],
|
||||
[1, 3, 0, 0, 2, 0, 2, 0, 4, 0, 0, 3, 1],
|
||||
[1, 0, 3, 0, 1, 4, 1, 4, 1, 1, 2, 1, 1],
|
||||
[1, 3, 0, 4, 1, 0, 4, 0, 0, 4, 0, 3, 1],
|
||||
[1, 1, 1, 0, 1, 3, 0, 1, 4, 1, 1, 1, 1],
|
||||
[1, 3, 1, 4, 1, 1, 0, 2, 0, 3, 1, 3, 1],
|
||||
[1, 0, 4, 0, 4, 0, 4, 1, 3, 0, 4, 0, 1],
|
||||
[1, 1, 0, 1, 0, 1, 0, 4, 0, 0, 1, 1, 1],
|
||||
[1, 3, 0, 1, 3, 1, 3, 0, 1, 4, 0, 3, 5],
|
||||
[1, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
// 地图 3:入口 (10,6) 与 (2,10)
|
||||
[
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 3, 1, 3, 1, 1, 1, 1, 3, 1, 0, 3, 1],
|
||||
[1, 2, 1, 2, 1, 1, 1, 1, 2, 1, 3, 3, 1],
|
||||
[1, 3, 0, 3, 4, 3, 4, 0, 3, 4, 3, 3, 1],
|
||||
[1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 3, 1, 0, 0, 4, 3, 1, 3, 0, 3, 1],
|
||||
[1, 1, 4, 1, 4, 1, 3, 1, 1, 0, 5, 0, 1],
|
||||
[1, 3, 3, 1, 3, 4, 0, 3, 2, 4, 0, 4, 1],
|
||||
[1, 2, 1, 1, 2, 1, 1, 4, 1, 1, 1, 2, 1],
|
||||
[1, 3, 0, 4, 3, 1, 1, 3, 1, 3, 1, 3, 1],
|
||||
[1, 0, 5, 1, 0, 1, 1, 0, 4, 0, 1, 0, 1],
|
||||
[1, 3, 1, 1, 3, 4, 3, 0, 1, 4, 2, 3, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
// 地图 4:入口 (6,1) 与 (6,11)
|
||||
[
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 0, 3, 2, 4, 0, 5, 0, 0, 4, 0, 3, 1],
|
||||
[1, 3, 1, 1, 2, 1, 0, 1, 2, 1, 3, 0, 1],
|
||||
[1, 0, 3, 1, 3, 4, 0, 4, 3, 1, 2, 1, 1],
|
||||
[1, 1, 4, 1, 2, 1, 3, 1, 2, 1, 0, 0, 1],
|
||||
[1, 0, 3, 4, 0, 0, 2, 0, 3, 1, 3, 3, 1],
|
||||
[1, 3, 0, 2, 0, 3, 1, 3, 0, 4, 0, 0, 1],
|
||||
[1, 0, 1, 0, 4, 1, 1, 1, 4, 1, 1, 1, 1],
|
||||
[1, 4, 1, 4, 3, 0, 1, 0, 3, 1, 0, 3, 1],
|
||||
[1, 0, 1, 3, 1, 3, 2, 3, 0, 1, 3, 0, 1],
|
||||
[1, 0, 1, 0, 1, 4, 0, 4, 1, 1, 1, 4, 1],
|
||||
[1, 3, 1, 3, 1, 3, 5, 3, 4, 0, 3, 0, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
// 地图 5:入口 (11,1) 与 (0,7)
|
||||
[
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 3, 0, 0, 4, 0, 1, 5, 1],
|
||||
[1, 1, 3, 3, 1, 0, 3, 1, 1, 3, 1, 0, 1],
|
||||
[1, 1, 3, 1, 1, 1, 2, 1, 0, 0, 4, 0, 1],
|
||||
[1, 1, 2, 1, 1, 3, 4, 1, 2, 1, 1, 1, 1],
|
||||
[1, 1, 4, 3, 1, 0, 3, 2, 0, 1, 0, 3, 1],
|
||||
[1, 1, 0, 1, 1, 1, 1, 1, 3, 2, 4, 3, 1],
|
||||
[5, 0, 4, 0, 3, 0, 0, 4, 0, 1, 0, 0, 1],
|
||||
[1, 4, 1, 4, 1, 1, 1, 1, 4, 1, 2, 1, 1],
|
||||
[1, 3, 1, 3, 1, 3, 0, 1, 0, 1, 2, 1, 1],
|
||||
[1, 3, 1, 0, 2, 0, 3, 4, 0, 4, 0, 3, 1],
|
||||
[1, 3, 2, 0, 1, 0, 0, 1, 3, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
// 地图 6:入口 (0,1) 与 (6,12),注意 (10,9) 的 6 按语义为空地
|
||||
[
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
[5, 2, 0, 3, 0, 4, 0, 4, 3, 3, 3, 0, 1],
|
||||
[1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 4, 1],
|
||||
[1, 3, 3, 0, 4, 1, 0, 1, 3, 3, 4, 0, 1],
|
||||
[1, 0, 1, 1, 2, 1, 4, 1, 1, 1, 1, 4, 1],
|
||||
[1, 4, 1, 3, 3, 1, 0, 1, 4, 0, 3, 0, 1],
|
||||
[1, 2, 1, 1, 1, 1, 0, 1, 2, 1, 1, 1, 1],
|
||||
[1, 0, 0, 0, 0, 2, 4, 2, 0, 0, 0, 0, 1],
|
||||
[1, 1, 2, 1, 1, 1, 2, 1, 1, 4, 0, 4, 1],
|
||||
[1, 3, 4, 3, 1, 3, 0, 3, 1, 1, 6, 1, 1],
|
||||
[1, 1, 2, 1, 1, 0, 1, 0, 1, 0, 3, 0, 1],
|
||||
[1, 3, 4, 3, 1, 3, 0, 3, 1, 0, 0, 0, 1],
|
||||
[1, 1, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 1]
|
||||
]
|
||||
];
|
||||
|
||||
describe('pathfinding performance', () => {
|
||||
// 验证各尺寸障碍地图上从建图到最优路径发现的完整寻路耗时均处于可用量级
|
||||
it('completes the full find pipeline across the map size matrix within the sanity bounds', () => {
|
||||
@ -395,4 +594,65 @@ describe('pathfinding performance', () => {
|
||||
expect(steps).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证用户提供的六张 13x13 真实游戏地图上入口到入口的分段寻路耗时与可达性
|
||||
it('measures graph build and search phases on the six real 13x13 game maps', () => {
|
||||
for (let i = 0; i < REAL_MAPS.length; i++) {
|
||||
const map = REAL_MAPS[i]!;
|
||||
const endpoints = resolveEndpoints(map, 13);
|
||||
const fixture = createPerformanceSystem(
|
||||
map.flat(),
|
||||
13,
|
||||
endpoints.start,
|
||||
REALMAP_TILE_DEFS
|
||||
);
|
||||
|
||||
// 伪随机抽选的端点需单独记录所选图块,保证运行结果可复现审查
|
||||
if (endpoints.seeded) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`[pathfinding-realmap] map=${i + 1} seed-picked ` +
|
||||
`start=(${endpoints.start.x},${endpoints.start.y}) ` +
|
||||
`target=(${endpoints.target.x},${endpoints.target.y})`
|
||||
);
|
||||
}
|
||||
|
||||
const buildTimes: number[] = [];
|
||||
const findTimes: number[] = [];
|
||||
let steps = 0;
|
||||
|
||||
for (let run = 0; run < REAL_RUNS; run++) {
|
||||
const build = measureCall('pathfinding-realmap-build', () =>
|
||||
fixture.buildGraph()
|
||||
);
|
||||
buildTimes.push(build.duration);
|
||||
|
||||
const find = measureCall('pathfinding-realmap-find', () =>
|
||||
fixture.system.getPath(endpoints.target)
|
||||
);
|
||||
findTimes.push(find.duration);
|
||||
steps = find.value.length;
|
||||
}
|
||||
|
||||
const searchAvg =
|
||||
findTimes.reduce(
|
||||
(sum, t, idx) => sum + (t - buildTimes[idx]!),
|
||||
0
|
||||
) / REAL_RUNS;
|
||||
|
||||
// 审查要求输出结构化真实地图性能数据供汇报使用
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`[pathfinding-realmap] map=${i + 1} 13x13 ` +
|
||||
`start=(${endpoints.start.x},${endpoints.start.y}) ` +
|
||||
`target=(${endpoints.target.x},${endpoints.target.y}) ` +
|
||||
`steps=${steps} build=${minAvg(buildTimes)} ` +
|
||||
`find=${minAvg(findTimes)} search=${searchAvg.toFixed(2)}ms ` +
|
||||
`unreachable=${steps === 0 ? 'yes' : 'no'}`
|
||||
);
|
||||
|
||||
// 真实地图不保证可达,仅对小图的耗时作宽松把关,不可达属有效数据
|
||||
expect(Math.max(...findTimes)).toBeLessThan(500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user