mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 23:24:39 +00:00
Make stepper authoritative for sparse motion segments
This commit is contained in:
@@ -1,67 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { GameMapImpl } from "../src/core/game/GameMap";
|
||||
import { densePathToKeypointSegments } from "../src/core/game/MotionPlans";
|
||||
import { MiniMapTransformer } from "../src/core/pathfinding/transformers/MiniMapTransformer";
|
||||
|
||||
function makeMap(width: number, height: number): GameMapImpl {
|
||||
return new GameMapImpl(width, height, new Uint8Array(width * height), 0);
|
||||
}
|
||||
|
||||
function expandPlanDda(
|
||||
map: GameMapImpl,
|
||||
points: Uint32Array,
|
||||
segmentSteps: Uint32Array,
|
||||
): number[] {
|
||||
const out: number[] = [];
|
||||
if (points.length === 0) return out;
|
||||
out.push(points[0] >>> 0);
|
||||
for (let i = 0; i < segmentSteps.length; i++) {
|
||||
const steps = segmentSteps[i] >>> 0;
|
||||
const a = points[i] >>> 0;
|
||||
const b = points[i + 1] >>> 0;
|
||||
const ax = map.x(a);
|
||||
const ay = map.y(a);
|
||||
const bx = map.x(b);
|
||||
const by = map.y(b);
|
||||
const dx = bx - ax;
|
||||
const dy = by - ay;
|
||||
for (let t = 1; t <= steps; t++) {
|
||||
out.push(
|
||||
map.ref(
|
||||
Math.round(ax + (dx * t) / steps),
|
||||
Math.round(ay + (dy * t) / steps),
|
||||
) >>> 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
describe("densePathToKeypointSegments", () => {
|
||||
it("expands back to the dense path for axis segments", () => {
|
||||
const map = makeMap(10, 10);
|
||||
|
||||
const dense = [
|
||||
map.ref(1, 1),
|
||||
map.ref(2, 1),
|
||||
map.ref(3, 1),
|
||||
map.ref(4, 1),
|
||||
map.ref(4, 2),
|
||||
map.ref(4, 3),
|
||||
map.ref(4, 4),
|
||||
];
|
||||
|
||||
const plan = densePathToKeypointSegments(dense);
|
||||
expect(plan).not.toBeNull();
|
||||
if (!plan) return;
|
||||
|
||||
const expanded = expandPlanDda(map, plan.points, plan.segmentSteps);
|
||||
expect(expanded).toEqual(dense.map((t) => t >>> 0));
|
||||
});
|
||||
});
|
||||
|
||||
describe("MiniMapTransformer planSegments compression", () => {
|
||||
it("preserves endpoints and total steps while merging collinear runs", () => {
|
||||
describe("MiniMapTransformer", () => {
|
||||
it("preserves dense path endpoints after upscaling/fixing extremes", () => {
|
||||
const map = makeMap(10, 10);
|
||||
const miniMap = makeMap(5, 5);
|
||||
|
||||
@@ -77,12 +23,6 @@ describe("MiniMapTransformer planSegments compression", () => {
|
||||
findPath() {
|
||||
return miniPath.slice();
|
||||
},
|
||||
planSegments() {
|
||||
return {
|
||||
points: Uint32Array.from(miniPath),
|
||||
segmentSteps: Uint32Array.from([1, 1, 1, 1]),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const transformer = new MiniMapTransformer(inner as any, map, miniMap);
|
||||
@@ -91,19 +31,8 @@ describe("MiniMapTransformer planSegments compression", () => {
|
||||
|
||||
const dense = transformer.findPath(from, to);
|
||||
expect(dense).not.toBeNull();
|
||||
|
||||
const plan = transformer.planSegments(from, to);
|
||||
expect(plan).not.toBeNull();
|
||||
if (!plan) return;
|
||||
|
||||
expect(Array.from(plan.points)).toEqual([
|
||||
from >>> 0,
|
||||
map.ref(4, 0) >>> 0,
|
||||
to >>> 0,
|
||||
]);
|
||||
expect(Array.from(plan.segmentSteps)).toEqual([4, 4]);
|
||||
|
||||
const totalSteps = Array.from(plan.segmentSteps).reduce((a, b) => a + b, 0);
|
||||
expect(totalSteps).toBe(8);
|
||||
if (!dense) return;
|
||||
expect(dense[0]).toBe(from);
|
||||
expect(dense[dense.length - 1]).toBe(to);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,4 +176,119 @@ describe("PathFinderStepper", () => {
|
||||
expect((result2 as { node: Pos }).node).toEqual({ x: 3, y: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("planSegments", () => {
|
||||
it("compresses dense paths into delta runs", () => {
|
||||
const path = [10, 11, 12, 13, 23, 33, 43];
|
||||
const stepper = new PathFinderStepper<number>({
|
||||
findPath: () => path.slice(),
|
||||
});
|
||||
|
||||
const plan = stepper.planSegments(10, 43);
|
||||
|
||||
expect(plan).not.toBeNull();
|
||||
if (!plan) return;
|
||||
expect(Array.from(plan.points)).toEqual([10, 13, 43]);
|
||||
expect(Array.from(plan.segmentSteps)).toEqual([3, 3]);
|
||||
});
|
||||
|
||||
it("reuses cached suffix after next() without an extra findPath call", () => {
|
||||
let calls = 0;
|
||||
const path = [1, 2, 3, 4, 14, 24];
|
||||
const stepper = new PathFinderStepper<number>({
|
||||
findPath: () => {
|
||||
calls++;
|
||||
return path.slice();
|
||||
},
|
||||
});
|
||||
|
||||
const r1 = stepper.next(1, 24);
|
||||
expect(r1.status).toBe(PathStatus.NEXT);
|
||||
const r2 = stepper.next(2, 24);
|
||||
expect(r2.status).toBe(PathStatus.NEXT);
|
||||
expect(calls).toBe(1);
|
||||
|
||||
const plan = stepper.planSegments(3, 24);
|
||||
|
||||
expect(plan).not.toBeNull();
|
||||
if (!plan) return;
|
||||
expect(calls).toBe(1);
|
||||
expect(Array.from(plan.points)).toEqual([3, 4, 24]);
|
||||
expect(Array.from(plan.segmentSteps)).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("prepends source when the returned dense path omits it", () => {
|
||||
const stepper = new PathFinderStepper<number>({
|
||||
findPath: () => [11, 12, 22],
|
||||
});
|
||||
|
||||
const plan = stepper.planSegments(10, 22);
|
||||
|
||||
expect(plan).not.toBeNull();
|
||||
if (!plan) return;
|
||||
expect(Array.from(plan.points)).toEqual([10, 12, 22]);
|
||||
expect(Array.from(plan.segmentSteps)).toEqual([2, 1]);
|
||||
});
|
||||
|
||||
it("skips zero-delta nodes while preserving run counts", () => {
|
||||
const stepper = new PathFinderStepper<number>({
|
||||
findPath: () => [10, 10, 11, 12, 22, 22, 32, 31],
|
||||
});
|
||||
|
||||
const plan = stepper.planSegments(10, 31);
|
||||
|
||||
expect(plan).not.toBeNull();
|
||||
if (!plan) return;
|
||||
expect(Array.from(plan.points)).toEqual([10, 12, 32, 31]);
|
||||
expect(Array.from(plan.segmentSteps)).toEqual([2, 2, 1]);
|
||||
});
|
||||
|
||||
it("returns a single-point plan when from equals to", () => {
|
||||
let calls = 0;
|
||||
const stepper = new PathFinderStepper<number>({
|
||||
findPath: () => {
|
||||
calls++;
|
||||
return [5];
|
||||
},
|
||||
});
|
||||
|
||||
const plan = stepper.planSegments(5, 5);
|
||||
|
||||
expect(plan).not.toBeNull();
|
||||
if (!plan) return;
|
||||
expect(calls).toBe(0);
|
||||
expect(Array.from(plan.points)).toEqual([5]);
|
||||
expect(plan.segmentSteps.length).toBe(0);
|
||||
});
|
||||
|
||||
it("returns null when no path exists", () => {
|
||||
const stepper = new PathFinderStepper<number>({
|
||||
findPath: () => null,
|
||||
});
|
||||
|
||||
const plan = stepper.planSegments(1, 99);
|
||||
expect(plan).toBeNull();
|
||||
});
|
||||
|
||||
it("supports multi-source by compressing the returned dense path once", () => {
|
||||
let calls = 0;
|
||||
const stepper = new PathFinderStepper<number>({
|
||||
findPath: (from) => {
|
||||
calls++;
|
||||
if (!Array.isArray(from)) {
|
||||
return null;
|
||||
}
|
||||
return [from[1], from[1] + 1, from[1] + 2];
|
||||
},
|
||||
});
|
||||
|
||||
const plan = stepper.planSegments([10, 20], 22);
|
||||
|
||||
expect(plan).not.toBeNull();
|
||||
if (!plan) return;
|
||||
expect(calls).toBe(1);
|
||||
expect(Array.from(plan.points)).toEqual([20, 22]);
|
||||
expect(Array.from(plan.segmentSteps)).toEqual([2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user