289 lines
9.0 KiB
TypeScript
289 lines
9.0 KiB
TypeScript
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||
|
|
import {
|
||
|
|
bresenhamLineOfSight,
|
||
|
|
raycastLineOfSight,
|
||
|
|
LineOfSightSmoother,
|
||
|
|
CatmullRomSmoother,
|
||
|
|
CombinedSmoother
|
||
|
|
} from '../../src/smoothing/PathSmoother';
|
||
|
|
import { GridMap } from '../../src/grid/GridMap';
|
||
|
|
import { createPoint, type IPoint } from '../../src/core/IPathfinding';
|
||
|
|
|
||
|
|
describe('Line of Sight Functions', () => {
|
||
|
|
let grid: GridMap;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
grid = new GridMap(10, 10);
|
||
|
|
});
|
||
|
|
|
||
|
|
// =========================================================================
|
||
|
|
// bresenhamLineOfSight
|
||
|
|
// =========================================================================
|
||
|
|
|
||
|
|
describe('bresenhamLineOfSight', () => {
|
||
|
|
it('should return true for clear line', () => {
|
||
|
|
expect(bresenhamLineOfSight(0, 0, 5, 5, grid)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return true for same point', () => {
|
||
|
|
expect(bresenhamLineOfSight(5, 5, 5, 5, grid)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return true for horizontal line', () => {
|
||
|
|
expect(bresenhamLineOfSight(0, 5, 9, 5, grid)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return true for vertical line', () => {
|
||
|
|
expect(bresenhamLineOfSight(5, 0, 5, 9, grid)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return false when blocked', () => {
|
||
|
|
grid.setWalkable(5, 5, false);
|
||
|
|
expect(bresenhamLineOfSight(0, 0, 9, 9, grid)).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return false when start is blocked', () => {
|
||
|
|
grid.setWalkable(0, 0, false);
|
||
|
|
expect(bresenhamLineOfSight(0, 0, 5, 5, grid)).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return false when end is blocked', () => {
|
||
|
|
grid.setWalkable(5, 5, false);
|
||
|
|
expect(bresenhamLineOfSight(0, 0, 5, 5, grid)).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should detect obstacle in middle', () => {
|
||
|
|
grid.setWalkable(3, 3, false);
|
||
|
|
expect(bresenhamLineOfSight(0, 0, 6, 6, grid)).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// =========================================================================
|
||
|
|
// raycastLineOfSight
|
||
|
|
// =========================================================================
|
||
|
|
|
||
|
|
describe('raycastLineOfSight', () => {
|
||
|
|
it('should return true for clear line', () => {
|
||
|
|
expect(raycastLineOfSight(0, 0, 5, 5, grid)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return true for same point', () => {
|
||
|
|
expect(raycastLineOfSight(5, 5, 5, 5, grid)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return false when blocked', () => {
|
||
|
|
grid.setWalkable(5, 5, false);
|
||
|
|
expect(raycastLineOfSight(0, 0, 9, 9, grid)).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should work with custom step size', () => {
|
||
|
|
expect(raycastLineOfSight(0, 0, 5, 5, grid, 0.1)).toBe(true);
|
||
|
|
grid.setWalkable(2, 2, false);
|
||
|
|
expect(raycastLineOfSight(0, 0, 5, 5, grid, 0.1)).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('LineOfSightSmoother', () => {
|
||
|
|
let grid: GridMap;
|
||
|
|
let smoother: LineOfSightSmoother;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
grid = new GridMap(20, 20);
|
||
|
|
smoother = new LineOfSightSmoother();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return same path for 2 or fewer points', () => {
|
||
|
|
const path1: IPoint[] = [createPoint(0, 0)];
|
||
|
|
expect(smoother.smooth(path1, grid)).toEqual(path1);
|
||
|
|
|
||
|
|
const path2: IPoint[] = [createPoint(0, 0), createPoint(5, 5)];
|
||
|
|
expect(smoother.smooth(path2, grid)).toEqual(path2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should remove unnecessary waypoints on straight line', () => {
|
||
|
|
const path: IPoint[] = [
|
||
|
|
createPoint(0, 0),
|
||
|
|
createPoint(1, 0),
|
||
|
|
createPoint(2, 0),
|
||
|
|
createPoint(3, 0),
|
||
|
|
createPoint(4, 0),
|
||
|
|
createPoint(5, 0)
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = smoother.smooth(path, grid);
|
||
|
|
expect(result.length).toBe(2);
|
||
|
|
expect(result[0]).toEqual(createPoint(0, 0));
|
||
|
|
expect(result[1]).toEqual(createPoint(5, 0));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should remove unnecessary waypoints on diagonal', () => {
|
||
|
|
const path: IPoint[] = [
|
||
|
|
createPoint(0, 0),
|
||
|
|
createPoint(1, 1),
|
||
|
|
createPoint(2, 2),
|
||
|
|
createPoint(3, 3),
|
||
|
|
createPoint(4, 4),
|
||
|
|
createPoint(5, 5)
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = smoother.smooth(path, grid);
|
||
|
|
expect(result.length).toBe(2);
|
||
|
|
expect(result[0]).toEqual(createPoint(0, 0));
|
||
|
|
expect(result[1]).toEqual(createPoint(5, 5));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should keep waypoints around obstacles', () => {
|
||
|
|
// Create obstacle
|
||
|
|
grid.setWalkable(5, 5, false);
|
||
|
|
|
||
|
|
const path: IPoint[] = [
|
||
|
|
createPoint(0, 0),
|
||
|
|
createPoint(4, 5),
|
||
|
|
createPoint(6, 5),
|
||
|
|
createPoint(10, 10)
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = smoother.smooth(path, grid);
|
||
|
|
// Should keep at least start, one waypoint near obstacle, and end
|
||
|
|
expect(result.length).toBeGreaterThanOrEqual(3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should use custom line of sight function', () => {
|
||
|
|
const customLOS = (x1: number, y1: number, x2: number, y2: number) => {
|
||
|
|
// Always blocked
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
|
||
|
|
const customSmoother = new LineOfSightSmoother(customLOS);
|
||
|
|
const path: IPoint[] = [
|
||
|
|
createPoint(0, 0),
|
||
|
|
createPoint(1, 1),
|
||
|
|
createPoint(2, 2)
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = customSmoother.smooth(path, grid);
|
||
|
|
// Should not simplify because LOS always fails
|
||
|
|
expect(result).toEqual(path);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('CatmullRomSmoother', () => {
|
||
|
|
let grid: GridMap;
|
||
|
|
let smoother: CatmullRomSmoother;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
grid = new GridMap(20, 20);
|
||
|
|
smoother = new CatmullRomSmoother(5, 0.5);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return same path for 2 or fewer points', () => {
|
||
|
|
const path1: IPoint[] = [createPoint(0, 0)];
|
||
|
|
expect(smoother.smooth(path1, grid)).toEqual(path1);
|
||
|
|
|
||
|
|
const path2: IPoint[] = [createPoint(0, 0), createPoint(5, 5)];
|
||
|
|
expect(smoother.smooth(path2, grid)).toEqual(path2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should add interpolation points', () => {
|
||
|
|
const path: IPoint[] = [
|
||
|
|
createPoint(0, 0),
|
||
|
|
createPoint(5, 0),
|
||
|
|
createPoint(10, 0)
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = smoother.smooth(path, grid);
|
||
|
|
// Should have more points due to interpolation
|
||
|
|
expect(result.length).toBeGreaterThan(path.length);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should preserve start and end points', () => {
|
||
|
|
const path: IPoint[] = [
|
||
|
|
createPoint(0, 0),
|
||
|
|
createPoint(5, 5),
|
||
|
|
createPoint(10, 0)
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = smoother.smooth(path, grid);
|
||
|
|
expect(result[0].x).toBeCloseTo(0, 1);
|
||
|
|
expect(result[0].y).toBeCloseTo(0, 1);
|
||
|
|
expect(result[result.length - 1]).toEqual(createPoint(10, 0));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should create smooth curve', () => {
|
||
|
|
const path: IPoint[] = [
|
||
|
|
createPoint(0, 0),
|
||
|
|
createPoint(5, 5),
|
||
|
|
createPoint(10, 0)
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = smoother.smooth(path, grid);
|
||
|
|
|
||
|
|
// Check that middle points are near the original waypoint
|
||
|
|
const middlePoints = result.filter(p =>
|
||
|
|
Math.abs(p.x - 5) < 2 && Math.abs(p.y - 5) < 2
|
||
|
|
);
|
||
|
|
expect(middlePoints.length).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should work with different segment counts', () => {
|
||
|
|
const smootherLow = new CatmullRomSmoother(2);
|
||
|
|
const smootherHigh = new CatmullRomSmoother(10);
|
||
|
|
|
||
|
|
const path: IPoint[] = [
|
||
|
|
createPoint(0, 0),
|
||
|
|
createPoint(5, 5),
|
||
|
|
createPoint(10, 0)
|
||
|
|
];
|
||
|
|
|
||
|
|
const resultLow = smootherLow.smooth(path, grid);
|
||
|
|
const resultHigh = smootherHigh.smooth(path, grid);
|
||
|
|
|
||
|
|
expect(resultHigh.length).toBeGreaterThan(resultLow.length);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('CombinedSmoother', () => {
|
||
|
|
let grid: GridMap;
|
||
|
|
let smoother: CombinedSmoother;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
grid = new GridMap(20, 20);
|
||
|
|
smoother = new CombinedSmoother(5, 0.5);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should first simplify then curve smooth', () => {
|
||
|
|
// Path with redundant points
|
||
|
|
const path: IPoint[] = [
|
||
|
|
createPoint(0, 0),
|
||
|
|
createPoint(1, 0),
|
||
|
|
createPoint(2, 0),
|
||
|
|
createPoint(3, 0),
|
||
|
|
createPoint(4, 0),
|
||
|
|
createPoint(5, 0),
|
||
|
|
createPoint(6, 3),
|
||
|
|
createPoint(7, 6),
|
||
|
|
createPoint(8, 6),
|
||
|
|
createPoint(9, 6),
|
||
|
|
createPoint(10, 6)
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = smoother.smooth(path, grid);
|
||
|
|
|
||
|
|
// Should have smoothed the path
|
||
|
|
expect(result.length).toBeGreaterThan(0);
|
||
|
|
expect(result[0].x).toBeCloseTo(0, 1);
|
||
|
|
expect(result[result.length - 1]).toEqual(createPoint(10, 6));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle simple path', () => {
|
||
|
|
const path: IPoint[] = [
|
||
|
|
createPoint(0, 0),
|
||
|
|
createPoint(10, 10)
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = smoother.smooth(path, grid);
|
||
|
|
expect(result.length).toBe(2);
|
||
|
|
});
|
||
|
|
});
|