From b62b394ee9b93893635478a28785c22ddf578f3e Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 5 Oct 2026 18:49:18 +0800 Subject: [PATCH] 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 --- .../src/path/__test__/graph.test.ts | 85 +++++++++---------- .../src/path/__test__/system.test.ts | 60 ++++++++----- packages-user/data-system/src/path/graph.ts | 10 ++- 3 files changed, 85 insertions(+), 70 deletions(-) diff --git a/packages-user/data-system/src/path/__test__/graph.test.ts b/packages-user/data-system/src/path/__test__/graph.test.ts index 078117a..02c7196 100644 --- a/packages-user/data-system/src/path/__test__/graph.test.ts +++ b/packages-user/data-system/src/path/__test__/graph.test.ts @@ -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); }); }); diff --git a/packages-user/data-system/src/path/__test__/system.test.ts b/packages-user/data-system/src/path/__test__/system.test.ts index 9a1da92..9b066a2 100644 --- a/packages-user/data-system/src/path/__test__/system.test.ts +++ b/packages-user/data-system/src/path/__test__/system.test.ts @@ -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); }); diff --git a/packages-user/data-system/src/path/graph.ts b/packages-user/data-system/src/path/graph.ts index f7d3fef..7aba9f3 100644 --- a/packages-user/data-system/src/path/graph.ts +++ b/packages-user/data-system/src/path/graph.ts @@ -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 = new Set(); const startIndex = indexer.locatorToIndex(start); adjacency.set(startIndex, []); + // 起始节点自身没有入边,须先登记进映射集否则会漏出图 + mapped.add(startIndex); // 以起始位置为中心 BFS,仅沿可通行有向边扩展,不可达区域不入图 const queue: ITileLocator[] = [start];