mirror of
https://github.com/motajs/template.git
synced 2026-10-21 14:50:26 +08:00
fix(08-07): align path graph/system tests and apply authorized D-10 fixes
- graph.ts: seed start node into mapped set (B1) and restore 174 NaN/negative cost guard (B2) - graph.test.ts: drop useMapState/DirectionMapper, inject Dir4/Dir8 handlers, reach genuine out-of-map branch and assert 183 - system.test.ts: drop finder.useMapState, inject Dir4FaceHandler, adapt to IPathfindingResult
This commit is contained in:
parent
4208857eff
commit
b62b394ee9
@ -11,7 +11,6 @@ import {
|
||||
type IPassCheckHandler,
|
||||
type IPassPredicate
|
||||
} from '@user/data-base';
|
||||
import { InternalDirectionGroup } from '@motajs/common';
|
||||
|
||||
vi.hoisted(() => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
@ -34,9 +33,9 @@ interface TestModules {
|
||||
MapState: typeof import('@user/data-base').MapState;
|
||||
TileStore: typeof import('@user/data-common').TileStore;
|
||||
FaceManager: typeof import('@user/data-common').FaceManager;
|
||||
Dir4FaceHandler: typeof import('@user/data-common').Dir4FaceHandler;
|
||||
Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler;
|
||||
RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder;
|
||||
DirectionMapper: typeof import('@motajs/common').DirectionMapper;
|
||||
logger: typeof import('@motajs/common').logger;
|
||||
}
|
||||
|
||||
@ -54,9 +53,9 @@ beforeAll(async () => {
|
||||
MapState: baseModule.MapState,
|
||||
TileStore: commonModule.TileStore,
|
||||
FaceManager: commonModule.FaceManager,
|
||||
Dir4FaceHandler: commonModule.Dir4FaceHandler,
|
||||
Dir8FaceHandler: commonModule.Dir8FaceHandler,
|
||||
RoleFaceBinder: commonModule.RoleFaceBinder,
|
||||
DirectionMapper: motaModule.DirectionMapper,
|
||||
logger: motaModule.logger
|
||||
};
|
||||
});
|
||||
@ -260,10 +259,9 @@ function createFixture(
|
||||
eventStore: {},
|
||||
roleFace: new modules.RoleFaceBinder(),
|
||||
faceManager,
|
||||
directionMapper: new modules.DirectionMapper(),
|
||||
saveSystem: {}
|
||||
} as never;
|
||||
const maps = new modules.MapState(tileStore, commonState);
|
||||
const maps = new modules.MapState(commonState);
|
||||
const map = maps.fromRaw({
|
||||
floorId: 'F1',
|
||||
width,
|
||||
@ -271,10 +269,10 @@ function createFixture(
|
||||
layerAlias: { 0: 'event' },
|
||||
events: { 0: {} }
|
||||
});
|
||||
const layer = map!.getLayerByAlias('event')!;
|
||||
const layer = map!.eventLayer!;
|
||||
const builder = new modules.PathfindingGraphBuilder();
|
||||
builder.useMapState(maps);
|
||||
builder.useMapLayer(layer);
|
||||
builder.useFaceHandler(new modules.Dir4FaceHandler());
|
||||
if (predicate) {
|
||||
builder.usePassPredicate(predicate);
|
||||
}
|
||||
@ -282,43 +280,36 @@ function createFixture(
|
||||
}
|
||||
|
||||
describe('pathfinding graph building', () => {
|
||||
// 验证未注入谓词时无可通行边,BFS 仅包含起始位置自身且无损失告警
|
||||
it('includes only the start node when no predicate is injected', () => {
|
||||
const { map, builder } = createFixture(
|
||||
// 验证仅注入方向处理器而未注入谓词时,构建入口告警缺失谓词守卫码 173 并返回 null
|
||||
it('warns the missing-predicate guard code and returns null without a predicate', () => {
|
||||
const { builder } = createFixture(
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
3,
|
||||
null
|
||||
);
|
||||
builder.useMapLayer(map.getLayerByAlias('event'));
|
||||
const graph = builder.build({ x: 1, y: 1 });
|
||||
const result = modules.logger.catch(() =>
|
||||
builder.build({ x: 1, y: 1 })
|
||||
);
|
||||
|
||||
expect(graph.width).toBe(3);
|
||||
expect(graph.height).toBe(3);
|
||||
expect(graph.nodes.size).toBe(1);
|
||||
const start = graph.nodes.get(1 * 3 + 1)!;
|
||||
expect(start.edges).toHaveLength(0);
|
||||
expect(start.terminal).toBe(false);
|
||||
expect(start.cost).toBe(1);
|
||||
expect(result.ret).toBeNull();
|
||||
expect(result.info.map(info => info.code)).toContain(173);
|
||||
});
|
||||
|
||||
// 验证注入谓词后中心节点邻域方向数与 DirectionMapper 四正交组一致为 4
|
||||
// 验证默认注入 Dir4FaceHandler 时中心节点拥有四条正交邻域边
|
||||
it('resolves four orthogonal neighbor edges for the center node', () => {
|
||||
const { map, builder } = createFixture(
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
3,
|
||||
null
|
||||
);
|
||||
const mapper = new modules.DirectionMapper();
|
||||
const expected = [...mapper.map(InternalDirectionGroup.Dir4)];
|
||||
const predicate = new FixturePredicate(
|
||||
map,
|
||||
new modules.Dir8FaceHandler()
|
||||
);
|
||||
builder.usePassPredicate(predicate);
|
||||
const graph = builder.build({ x: 1, y: 1 });
|
||||
const graph = builder.build({ x: 1, y: 1 })!;
|
||||
|
||||
const center = graph.nodes.get(1 * 3 + 1)!;
|
||||
expect(center.edges).toHaveLength(expected.length);
|
||||
expect(center.edges).toHaveLength(4);
|
||||
const dirs = new Set(center.edges.map(edge => edge.dir));
|
||||
expect(dirs).toEqual(
|
||||
@ -331,8 +322,8 @@ describe('pathfinding graph building', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// 验证 useDirGroup 注入八方向组后中心节点拥有 8 条邻域边
|
||||
it('expands neighbor edges to eight when Dir8 group is injected', () => {
|
||||
// 验证注入 Dir8FaceHandler 后中心节点拥有 8 条邻域边
|
||||
it('expands neighbor edges to eight when the Dir8 handler is injected', () => {
|
||||
const { map, builder } = createFixture(
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
3,
|
||||
@ -343,8 +334,8 @@ describe('pathfinding graph building', () => {
|
||||
new modules.Dir8FaceHandler()
|
||||
);
|
||||
builder.usePassPredicate(predicate);
|
||||
builder.useFaceHandler(InternalDirectionGroup.Dir8);
|
||||
const graph = builder.build({ x: 1, y: 1 });
|
||||
builder.useFaceHandler(new modules.Dir8FaceHandler());
|
||||
const graph = builder.build({ x: 1, y: 1 })!;
|
||||
|
||||
const center = graph.nodes.get(1 * 3 + 1)!;
|
||||
expect(center.edges).toHaveLength(8);
|
||||
@ -358,7 +349,7 @@ describe('pathfinding graph building', () => {
|
||||
new modules.Dir8FaceHandler()
|
||||
);
|
||||
builder.usePassPredicate(predicate);
|
||||
const graph = builder.build({ x: 0, y: 0 });
|
||||
const graph = builder.build({ x: 0, y: 0 })!;
|
||||
|
||||
const source = graph.nodes.get(0)!;
|
||||
const sink = graph.nodes.get(1)!;
|
||||
@ -375,7 +366,7 @@ describe('pathfinding graph building', () => {
|
||||
new modules.Dir8FaceHandler()
|
||||
);
|
||||
builder.usePassPredicate(predicate);
|
||||
const graph = builder.build({ x: 0, y: 0 });
|
||||
const graph = builder.build({ x: 0, y: 0 })!;
|
||||
|
||||
const hit = graph.nodes.get(1)!;
|
||||
expect(hit.terminal).toBe(true);
|
||||
@ -387,8 +378,8 @@ describe('pathfinding graph building', () => {
|
||||
expect(graph.nodes.get(2)!.terminal).toBe(false);
|
||||
});
|
||||
|
||||
// 验证图层未绑定时构建入口告警新码 173 并返回空图而非异常
|
||||
it('warns the registered code and returns an empty graph without layer', () => {
|
||||
// 验证未绑定图层时构建入口告警守卫码 173 并返回 null
|
||||
it('warns the registered code and returns null without layer', () => {
|
||||
const builder = new modules.PathfindingGraphBuilder();
|
||||
builder.useMapLayer(null);
|
||||
|
||||
@ -396,21 +387,27 @@ describe('pathfinding graph building', () => {
|
||||
builder.build({ x: 0, y: 0 })
|
||||
);
|
||||
|
||||
expect(result.ret.nodes.size).toBe(0);
|
||||
expect(result.ret).toBeNull();
|
||||
expect(result.info.map(info => info.code)).toContain(173);
|
||||
});
|
||||
|
||||
// 验证起始位置越界时构建入口同样告警新码 173 并返回空图
|
||||
it('warns the registered code and returns an empty graph when start is out of map', () => {
|
||||
const { builder } = createFixture([1, 1, 1, 1, 1, 1, 1, 1, 1], 3, null);
|
||||
// 验证谓词与方向处理器齐备而起始位置越界时,构建入口告警越界守卫码 183 并返回 null
|
||||
it('warns the out-of-map guard code and returns null when start is out of map', () => {
|
||||
const { map, builder } = createFixture(
|
||||
[1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
3,
|
||||
null
|
||||
);
|
||||
builder.usePassPredicate(
|
||||
new FixturePredicate(map, new modules.Dir8FaceHandler())
|
||||
);
|
||||
|
||||
const result = modules.logger.catch(() =>
|
||||
builder.build({ x: 3, y: 0 })
|
||||
);
|
||||
|
||||
expect(result.ret.nodes.size).toBe(0);
|
||||
expect(result.ret.width).toBe(0);
|
||||
expect(result.info.map(info => info.code)).toContain(173);
|
||||
expect(result.ret).toBeNull();
|
||||
expect(result.info.map(info => info.code)).toContain(183);
|
||||
});
|
||||
|
||||
// 验证墙体隔断区域不进入有向图:图仅包含从起始位置沿可通行边可达的节点
|
||||
@ -423,7 +420,7 @@ describe('pathfinding graph building', () => {
|
||||
builder.usePassPredicate(
|
||||
new FixturePredicate(map, new modules.Dir8FaceHandler())
|
||||
);
|
||||
const graph = builder.build({ x: 0, y: 0 });
|
||||
const graph = builder.build({ x: 0, y: 0 })!;
|
||||
|
||||
expect(graph.nodes.size).toBe(3);
|
||||
expect(graph.nodes.has(0)).toBe(true);
|
||||
@ -447,7 +444,7 @@ describe('pathfinding graph building', () => {
|
||||
builder.useCostFunction(block =>
|
||||
block.locator.x === 1 && block.locator.y === 1 ? 7 : 3
|
||||
);
|
||||
const graph = builder.build({ x: 1, y: 1 });
|
||||
const graph = builder.build({ x: 1, y: 1 })!;
|
||||
|
||||
expect(graph.nodes.get(1 * 3 + 1)!.cost).toBe(7);
|
||||
expect(graph.nodes.get(0)!.cost).toBe(3);
|
||||
@ -475,7 +472,7 @@ describe('pathfinding graph building', () => {
|
||||
);
|
||||
|
||||
expect(result.info.map(info => info.code)).not.toContain(174);
|
||||
expect(result.ret.nodes.get(1 * 3 + 1)!.cost).toBe(
|
||||
expect(result.ret!.nodes.get(1 * 3 + 1)!.cost).toBe(
|
||||
Number.POSITIVE_INFINITY
|
||||
);
|
||||
});
|
||||
@ -499,7 +496,7 @@ describe('pathfinding graph building', () => {
|
||||
);
|
||||
|
||||
expect(result.info.filter(info => info.code === 174)).toHaveLength(1);
|
||||
expect(result.ret.nodes.get(1 * 3 + 1)!.cost).toBe(1);
|
||||
expect(result.ret!.nodes.get(1 * 3 + 1)!.cost).toBe(1);
|
||||
});
|
||||
|
||||
// 验证负数损失同样在构建时告警新码 174 并回退为损失 1
|
||||
@ -521,6 +518,6 @@ describe('pathfinding graph building', () => {
|
||||
);
|
||||
|
||||
expect(result.info.filter(info => info.code === 174)).toHaveLength(1);
|
||||
expect(result.ret.nodes.get(1 * 3 + 1)!.cost).toBe(1);
|
||||
expect(result.ret!.nodes.get(1 * 3 + 1)!.cost).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -18,7 +18,11 @@ import {
|
||||
type IPassPredicate
|
||||
} from '@user/data-base';
|
||||
import { type PathfindingSystem } from '../system';
|
||||
import { type IPathfindingStep, type PathCostFunction } from '../types';
|
||||
import {
|
||||
PathfindingStatus,
|
||||
type IPathfindingStep,
|
||||
type PathCostFunction
|
||||
} from '../types';
|
||||
|
||||
vi.hoisted(() => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
@ -42,8 +46,8 @@ interface TestModules {
|
||||
TileStore: typeof import('@user/data-common').TileStore;
|
||||
ObjectMover: typeof import('@user/data-common').ObjectMover;
|
||||
FaceManager: typeof import('@user/data-common').FaceManager;
|
||||
Dir4FaceHandler: typeof import('@user/data-common').Dir4FaceHandler;
|
||||
Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler;
|
||||
DirectionMapper: typeof import('@motajs/common').DirectionMapper;
|
||||
RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder;
|
||||
logger: typeof import('@motajs/common').logger;
|
||||
}
|
||||
@ -63,8 +67,8 @@ beforeAll(async () => {
|
||||
TileStore: commonModule.TileStore,
|
||||
ObjectMover: commonModule.ObjectMover,
|
||||
FaceManager: commonModule.FaceManager,
|
||||
Dir4FaceHandler: commonModule.Dir4FaceHandler,
|
||||
Dir8FaceHandler: commonModule.Dir8FaceHandler,
|
||||
DirectionMapper: motaModule.DirectionMapper,
|
||||
RoleFaceBinder: commonModule.RoleFaceBinder,
|
||||
logger: motaModule.logger
|
||||
};
|
||||
@ -321,10 +325,9 @@ function createSystem(rows: number[], width: number): SystemFixture {
|
||||
eventStore: {},
|
||||
roleFace: new modules.RoleFaceBinder(),
|
||||
faceManager,
|
||||
directionMapper: new modules.DirectionMapper(),
|
||||
saveSystem: {}
|
||||
} as never;
|
||||
const maps = new modules.MapState(tileStore, commonState);
|
||||
const maps = new modules.MapState(commonState);
|
||||
const map = maps.fromRaw({
|
||||
floorId: 'F1',
|
||||
width,
|
||||
@ -332,12 +335,12 @@ function createSystem(rows: number[], width: number): SystemFixture {
|
||||
layerAlias: { 0: 'event' },
|
||||
events: { 0: {} }
|
||||
})!;
|
||||
const layer = map.getLayerByAlias('event')!;
|
||||
const layer = map.eventLayer!;
|
||||
const system = new modules.PathfindingSystem(commonState as never);
|
||||
const tile = createTestTile();
|
||||
system.useMover(tile.mover);
|
||||
system.finder.useMapState(maps);
|
||||
system.finder.useMapLayer(layer);
|
||||
system.finder.useFaceHandler(new modules.Dir4FaceHandler());
|
||||
return { map, layer, system, tile };
|
||||
}
|
||||
|
||||
@ -359,12 +362,13 @@ describe('pathfinding system', () => {
|
||||
const fixture = createSystem([1, 1, 1, 1, 1, 1, 1, 1, 1], 3);
|
||||
injectPredicate(fixture);
|
||||
|
||||
const steps = fixture.system.finder.find(
|
||||
const result = fixture.system.finder.find(
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 2, y: 0 }
|
||||
);
|
||||
|
||||
expect(steps).toEqual([
|
||||
expect(result.status).toBe(PathfindingStatus.Success);
|
||||
expect(result.path).toEqual([
|
||||
{
|
||||
dir: FaceDirection.Right,
|
||||
from: { x: 0, y: 0 },
|
||||
@ -386,13 +390,14 @@ describe('pathfinding system', () => {
|
||||
block.locator.x === 1 && block.locator.y === 1 ? 10 : 1;
|
||||
fixture.system.finder.useCostFunction(cost);
|
||||
|
||||
const steps = fixture.system.finder.find(
|
||||
const result = fixture.system.finder.find(
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 2, y: 1 }
|
||||
);
|
||||
|
||||
expect(steps).toHaveLength(4);
|
||||
for (const step of steps) {
|
||||
expect(result.status).toBe(PathfindingStatus.Success);
|
||||
expect(result.path).toHaveLength(4);
|
||||
for (const step of result.path) {
|
||||
expect(step.to).not.toEqual({ x: 1, y: 1 });
|
||||
}
|
||||
});
|
||||
@ -410,7 +415,8 @@ describe('pathfinding system', () => {
|
||||
);
|
||||
|
||||
expect(result.info.map(info => info.code)).toContain(174);
|
||||
expect(result.ret).toHaveLength(2);
|
||||
expect(result.ret.status).toBe(PathfindingStatus.Success);
|
||||
expect(result.ret.path).toHaveLength(2);
|
||||
});
|
||||
|
||||
// 验证 Infinity 是合法损失值:不告警且路径绕开高损失格
|
||||
@ -428,8 +434,9 @@ describe('pathfinding system', () => {
|
||||
);
|
||||
|
||||
expect(result.info.map(info => info.code)).not.toContain(174);
|
||||
expect(result.ret).toHaveLength(4);
|
||||
for (const step of result.ret) {
|
||||
expect(result.ret.status).toBe(PathfindingStatus.Success);
|
||||
expect(result.ret.path).toHaveLength(4);
|
||||
for (const step of result.ret.path) {
|
||||
expect(step.to).not.toEqual({ x: 1, y: 1 });
|
||||
}
|
||||
});
|
||||
@ -447,8 +454,10 @@ describe('pathfinding system', () => {
|
||||
);
|
||||
const path = fixture.system.getPath({ x: 2, y: 1 });
|
||||
|
||||
expect(steps).toEqual([]);
|
||||
expect(path).toEqual([]);
|
||||
expect(steps.status).toBe(PathfindingStatus.NoPath);
|
||||
expect(steps.path).toEqual([]);
|
||||
expect(path.status).toBe(PathfindingStatus.NoPath);
|
||||
expect(path.path).toEqual([]);
|
||||
expect(fixture.tile.x).toBe(0);
|
||||
expect(fixture.tile.y).toBe(0);
|
||||
expect(fixture.tile.setPosCalls).toEqual([]);
|
||||
@ -463,8 +472,9 @@ describe('pathfinding system', () => {
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 2, y: 0 }
|
||||
);
|
||||
expect(detour).toHaveLength(4);
|
||||
for (const step of detour) {
|
||||
expect(detour.status).toBe(PathfindingStatus.Success);
|
||||
expect(detour.path).toHaveLength(4);
|
||||
for (const step of detour.path) {
|
||||
expect(step.to).not.toEqual({ x: 1, y: 0 });
|
||||
}
|
||||
|
||||
@ -472,7 +482,8 @@ describe('pathfinding system', () => {
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 0 }
|
||||
);
|
||||
expect(direct).toEqual([
|
||||
expect(direct.status).toBe(PathfindingStatus.Success);
|
||||
expect(direct.path).toEqual([
|
||||
{
|
||||
dir: FaceDirection.Right,
|
||||
from: { x: 0, y: 0 },
|
||||
@ -489,7 +500,8 @@ describe('pathfinding system', () => {
|
||||
const result = fixture.system.moveTo({ x: 2, y: 0 });
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.path).toHaveLength(2);
|
||||
expect(result!.path.status).toBe(PathfindingStatus.Success);
|
||||
expect(result!.path.path).toHaveLength(2);
|
||||
await result!.controller.onEnd;
|
||||
expect(fixture.tile.x).toBe(2);
|
||||
expect(fixture.tile.y).toBe(0);
|
||||
@ -579,9 +591,11 @@ describe('pathfinding system', () => {
|
||||
system.getPath({ x: 1, y: 0 })
|
||||
);
|
||||
|
||||
expect(findResult.ret).toEqual([]);
|
||||
expect(findResult.ret.status).toBe(PathfindingStatus.InvalidInput);
|
||||
expect(findResult.ret.path).toEqual([]);
|
||||
expect(findResult.info.map(info => info.code)).toContain(173);
|
||||
expect(pathResult.ret).toEqual([]);
|
||||
expect(pathResult.ret.status).toBe(PathfindingStatus.InvalidInput);
|
||||
expect(pathResult.ret.path).toEqual([]);
|
||||
expect(pathResult.info.map(info => info.code)).toContain(173);
|
||||
});
|
||||
|
||||
|
||||
@ -55,11 +55,13 @@ export class MapGraphBuilder implements IMapGraphBuilder {
|
||||
* @param block 位置信息
|
||||
*/
|
||||
private resolveCost(block: ILayerLocation): number {
|
||||
if (this.cost) {
|
||||
return this.cost(block);
|
||||
} else {
|
||||
const cost = this.cost ? this.cost(block) : 1;
|
||||
// Infinity 是合法损失,仅 NaN 或负数视为非法并回退为单位损失
|
||||
if (Number.isNaN(cost) || cost < 0) {
|
||||
logger.warn(174);
|
||||
return 1;
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
build(start: ITileLocator): IMapGraph | null {
|
||||
@ -91,6 +93,8 @@ export class MapGraphBuilder implements IMapGraphBuilder {
|
||||
const mapped: Set<number> = new Set();
|
||||
const startIndex = indexer.locatorToIndex(start);
|
||||
adjacency.set(startIndex, []);
|
||||
// 起始节点自身没有入边,须先登记进映射集否则会漏出图
|
||||
mapped.add(startIndex);
|
||||
|
||||
// 以起始位置为中心 BFS,仅沿可通行有向边扩展,不可达区域不入图
|
||||
const queue: ITileLocator[] = [start];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user