diff --git a/README.md b/README.md
index f79264b..06412bf 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Finders:
Jump point: 10
```ts
-const grid = new Grid([
+const grid = Grid.from([
[50, 55, 60, 65, 70],
[45, 0, 0, 0, 75],
[40, 0, 0, 0, 1], //x4y2
@@ -23,7 +23,7 @@ const grid = new Grid([
#### PF example 1
```ts
-grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, 10, FinderEnum.JUMP_POINT);
+grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, {finder: FinderEnum.JUMP_POINT, maxJumpCost: 10});
```
```js
@@ -40,7 +40,7 @@ grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, 10, FinderEnum.JUMP_POINT);
#### PF example 2
```ts
-grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, 1);
+grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, {maxJumpCost: 1});
```
```js
@@ -50,7 +50,7 @@ grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, 1);
#### PF example 3
```ts
-grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, 85);
+grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, {maxJumpCost: 85});
```
```js
diff --git a/__tests__/jumpPoint.test.ts b/__tests__/jumpPoint.test.ts
index 46342f5..d320c16 100644
--- a/__tests__/jumpPoint.test.ts
+++ b/__tests__/jumpPoint.test.ts
@@ -1,6 +1,6 @@
import { smallGrid, testCasesSmallGrid } from "./test-data/small-grid.ts";
import { testCasesBigGrid } from "./test-data/big-grid.ts";
-import { Grid } from "../mod.ts";
+import {FinderEnum, Grid} from "../mod.ts";
import { assertEquals } from "std/assert/assert_equals.ts";
import { describe, it } from "std/testing/bdd.ts";
@@ -12,8 +12,8 @@ describe("Diagonal Jump Point", () => {
testCases.forEach(
({ startPoint, endPoint, maxJumpCost, path: expectedPath, grid }) => {
it(`validates pathfinding from {${Object.values(startPoint)}} to {${Object.values(endPoint)}} with jumpCost {${maxJumpCost}}`, () => {
- const testGrid = new Grid(grid);
- const path = testGrid.findPath(startPoint, endPoint, maxJumpCost);
+ const testGrid = Grid.from(grid);
+ const path = testGrid.findPath(startPoint, endPoint, {finder: FinderEnum.JUMP_POINT, maxJumpCost});
// drawLayout(grid, path)
assertEquals(JSON.stringify(path), JSON.stringify(expectedPath));
});
@@ -21,21 +21,21 @@ describe("Diagonal Jump Point", () => {
);
it("throws an error if start point does no exist", () => {
- const grid = new Grid(smallGrid);
- const find = () => grid.findPath({ x: 999, y: 999 }, { x: 4, y: 1 });
+ const grid = Grid.from(smallGrid);
+ const find = () => grid.findPath({ x: 999, y: 999 }, { x: 4, y: 1 }, {finder: FinderEnum.JUMP_POINT});
assertThrows(find, Error, "startNode does not exist in the grid");
});
it("throws an error if end point does no exist", () => {
- const grid = new Grid(smallGrid);
- const find = () => grid.findPath({ x: 4, y: 1 }, { x: 999, y: 999 });
+ const grid = Grid.from(smallGrid);
+ const find = () => grid.findPath({ x: 4, y: 1 }, { x: 999, y: 999 }, {finder: FinderEnum.JUMP_POINT});
assertThrows(find, Error, "endNode does not exist in the grid");
});
it("throws an error if grid is empty", () => {
- const createEmptyGrid = () => new Grid([]);
+ const createEmptyGrid = () => Grid.from([]);
assertThrows(createEmptyGrid, Error, "grid matrix cannot be empty");
});
});
diff --git a/__tests__/openList.test.ts b/__tests__/openList.test.ts
index 74e6a2f..b9e59e7 100644
--- a/__tests__/openList.test.ts
+++ b/__tests__/openList.test.ts
@@ -17,12 +17,12 @@ describe("OpenList", () => {
});
it("is empty", () => {
- assertEquals(list.empty(), true);
+ assertEquals(list.isEmpty(), true);
});
it("is not empty", () => {
list.push(1);
- assertEquals(list.empty(), false);
+ assertEquals(list.isEmpty(), false);
});
it("removes lowest value", () => {
diff --git a/res/debug_view.html b/res/debug_view.html
new file mode 100644
index 0000000..6c3b9c3
--- /dev/null
+++ b/res/debug_view.html
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+ Pathfinder visualization
+
+
+
+
+
+
+
diff --git a/src/finders/custom.finder.ts b/src/finders/custom.finder.ts
new file mode 100644
index 0000000..cdd03cc
--- /dev/null
+++ b/src/finders/custom.finder.ts
@@ -0,0 +1,211 @@
+import { Point } from "../objects/point/point.ts";
+import { Grid } from "../objects/grid.ts";
+import { PointInterface } from "../objects/point/point.interface.ts";
+import { OpenList } from "../objects/openList.ts";
+import { FindPathConfig } from "./finder.types.ts";
+
+const NOT_REACHED_COST: number = 999999;
+const MAX_JUMP_HEIGHT: number = 5;
+
+class PathNode {
+ public from: PathNode | null;
+ public point: PointInterface;
+ public cost: number;
+ public heuristicValue: number;
+
+ constructor(
+ point: PointInterface,
+ from: PathNode | null,
+ cost: number,
+ heuristicValue: number,
+ ) {
+ this.from = from;
+ this.point = point;
+ this.cost = cost;
+ this.heuristicValue = heuristicValue;
+ }
+
+ toString(): string {
+ return `(${this.point.x}, ${this.point.y}, cost: ${this.cost}, heuristic: ${this.heuristicValue})`;
+ }
+}
+
+export const findPath = (
+ startPoint: PointInterface,
+ endPoint: PointInterface,
+ grid: Grid,
+ config: FindPathConfig,
+): PointInterface[] => {
+ const diagonalCostMultiplier = config.diagonalCostMultiplier ?? 1;
+ const orthogonalCostMultiplier = config.orthogonalCostMultiplier ?? 1;
+ const maxJumpCost = config.maxJumpCost ?? 5;
+ const maxIterations = config.maxIterations ?? 99999;
+
+ const index = (point: PointInterface): number => {
+ return point.y * grid.height + point.x;
+ };
+
+ const getMoveCostAt = (
+ src: PointInterface,
+ dst: PointInterface,
+ ): number | null => {
+ if (!grid.inBounds(src) || !grid.inBounds(dst)) return null;
+
+ // Difference in height
+ const srcHeight = grid.getHeightAt(src) ?? NOT_REACHED_COST;
+ const dstHeight = grid.getHeightAt(dst) ?? NOT_REACHED_COST;
+
+ // Max jump
+ if (Math.abs(srcHeight - dstHeight) > MAX_JUMP_HEIGHT) return null;
+
+ return 1 + Math.abs(srcHeight - dstHeight);
+ };
+
+ // Orthogonal jumps from JumpPoint
+ const addOrthogonalJumps = (
+ prevNode: PathNode,
+ src: PointInterface,
+ srcCost: number,
+ dirX: number,
+ dirY: number,
+ ) => {
+ let jumpDistance = 1;
+ let accumulatedCost = 0;
+ let prevPoint = src;
+
+ while (true) {
+ const target = new Point(
+ src.x + dirX * jumpDistance,
+ src.y + dirY * jumpDistance,
+ );
+
+ if (!grid.isWalkable(target)) {
+ break;
+ }
+
+ const moveCost = getMoveCostAt(prevPoint, target);
+ if (moveCost === null) {
+ break;
+ }
+
+ accumulatedCost += moveCost * orthogonalCostMultiplier;
+ const targetIndex = index(target);
+ const totalCost = srcCost + accumulatedCost;
+
+ if (totalCost < visited[targetIndex]) {
+ visited[targetIndex] = totalCost;
+ queue.push(
+ new PathNode(target, prevNode, totalCost, heuristic(target)),
+ );
+ }
+ prevPoint = target;
+ jumpDistance++;
+
+ if (accumulatedCost > maxJumpCost) {
+ break;
+ }
+ }
+ };
+
+ // Diagonal movements with filter for adjacent walkable tiles in the same floor height
+ const addDiagonal = (
+ prevNode: PathNode,
+ src: PointInterface,
+ srcCost: number,
+ dirX: number,
+ dirY: number,
+ ) => {
+ const target = new Point(src.x + dirX, src.y + dirY);
+ const moveCost =
+ srcCost +
+ (getMoveCostAt(src, target) ?? NOT_REACHED_COST) * diagonalCostMultiplier;
+ const targetHeight = grid.getHeightAt(target);
+ const aux1 = new Point(src.x, src.y + dirY);
+ const aux2 = new Point(src.x + dirX, src.y);
+ const targetIndex = index(target);
+
+ if (
+ grid.isWalkable(target) &&
+ grid.isWalkable(aux1) &&
+ grid.isWalkable(aux2) &&
+ targetHeight == grid.getHeightAt(aux1) &&
+ targetHeight == grid.getHeightAt(aux2) &&
+ moveCost < visited[targetIndex]
+ ) {
+ visited[targetIndex] = moveCost;
+ queue.push(new PathNode(target, prevNode, moveCost, heuristic(target)));
+ }
+ };
+
+ if (!grid.isWalkable(startPoint) || !grid.isWalkable(endPoint)) {
+ return [];
+ }
+
+ const visited = new Float32Array(grid.width * grid.height);
+ visited.fill(NOT_REACHED_COST);
+ config.travelCosts = visited;
+
+ // Distance to the end point, from A*
+ const travelHeuristic = new Float32Array(grid.width * grid.height);
+ travelHeuristic.fill(NOT_REACHED_COST);
+
+ grid.walkMatrix((x, y) => {
+ travelHeuristic[y * grid.height + x] = grid.distance(
+ new Point(x, y),
+ endPoint,
+ );
+ });
+ config.travelHeuristic = travelHeuristic;
+
+ const heuristic = (a: PointInterface): number => {
+ return travelHeuristic[index(a)];
+ };
+
+ const comparator = (a: PathNode, b: PathNode): number => {
+ return a.cost + a.heuristicValue - (b.cost + b.heuristicValue);
+ };
+
+ const queue: OpenList = new OpenList(comparator);
+ let iterations = 0;
+
+ visited[index(startPoint)] = 0;
+ queue.push(new PathNode(startPoint, null, 0, heuristic(startPoint)));
+
+ while (!queue.isEmpty()) {
+ if (iterations > maxIterations) {
+ return [];
+ }
+ iterations++;
+
+ const node = queue.pop();
+ const point = node.point;
+
+ // If we reached the end point, return the path to it
+ if (point.x === endPoint.x && point.y === endPoint.y) {
+ return pathFromNode(node);
+ }
+
+ addOrthogonalJumps(node, point, node.cost, 0, -1);
+ addOrthogonalJumps(node, point, node.cost, 0, 1);
+ addOrthogonalJumps(node, point, node.cost, -1, 0);
+ addOrthogonalJumps(node, point, node.cost, 1, 0);
+
+ addDiagonal(node, point, node.cost, 1, 1);
+ addDiagonal(node, point, node.cost, -1, 1);
+ addDiagonal(node, point, node.cost, 1, -1);
+ addDiagonal(node, point, node.cost, -1, -1);
+ }
+
+ // No path found
+ return [];
+};
+
+const pathFromNode = (lastNode: PathNode): PointInterface[] => {
+ const path: PointInterface[] = [];
+ let node: PathNode | null = lastNode;
+ while (node) {
+ path.push(node.point);
+ node = node.from;
+ }
+ return path.reverse();
+};
diff --git a/src/finders/finder.enum.ts b/src/finders/finder.enum.ts
index 7835755..bc0bc19 100644
--- a/src/finders/finder.enum.ts
+++ b/src/finders/finder.enum.ts
@@ -1,3 +1,4 @@
export enum FinderEnum {
JUMP_POINT,
+ CUSTOM,
}
diff --git a/src/finders/finder.types.ts b/src/finders/finder.types.ts
new file mode 100644
index 0000000..5371315
--- /dev/null
+++ b/src/finders/finder.types.ts
@@ -0,0 +1,15 @@
+import { FinderEnum } from "./finder.enum.ts";
+
+export type FindPathConfig = {
+ finder: FinderEnum;
+
+ // JumpPoint Finder
+ maxJumpCost?: number;
+
+ // Custom Finder
+ orthogonalCostMultiplier?: number;
+ diagonalCostMultiplier?: number;
+ maxIterations?: number;
+ travelCosts?: Float32Array;
+ travelHeuristic?: Float32Array;
+};
diff --git a/src/finders/jumpPoint.finder.ts b/src/finders/jumpPoint.finder.ts
index ee1e04d..b37abf0 100644
--- a/src/finders/jumpPoint.finder.ts
+++ b/src/finders/jumpPoint.finder.ts
@@ -3,6 +3,8 @@ import { Point } from "../objects/point/point.ts";
import { PointInterface } from "../objects/point/point.interface.ts";
import { DirectionNode } from "../objects/nodes/directionNode.ts";
import { DirectionEnum } from "../objects/nodes/direction.enum.ts";
+import { Node } from "../objects/nodes/node.ts";
+import { FindPathConfig } from "./finder.types.ts";
const possibleDirections: DirectionEnum[] = [
DirectionEnum.NORTH,
@@ -16,17 +18,26 @@ const possibleDirections: DirectionEnum[] = [
];
export const findPath = (
- startPoint: Point,
- endPoint: Point,
+ startPoint: PointInterface,
+ endPoint: PointInterface,
grid: Grid,
+ config: FindPathConfig,
): PointInterface[] => {
- const startNode: DirectionNode | null = grid.getNode(
- startPoint,
- ) as DirectionNode;
- const endNode: DirectionNode | null = grid.getNode(endPoint) as DirectionNode;
+ const nodes: (DirectionNode | null)[][] = buildNodes(
+ grid,
+ config.maxJumpCost ?? 5,
+ );
+ const getNode = (point: PointInterface): Node | DirectionNode | null => {
+ if (nodes[point.y] === undefined) return null;
+ return nodes[point.y][point.x];
+ };
+
+ const startNode: DirectionNode | null = getNode(startPoint) as DirectionNode;
+ const endNode: DirectionNode | null = getNode(endPoint) as DirectionNode;
- if (startNode === null)
+ if (startNode === null) {
throw new Error("startNode does not exist in the grid");
+ }
if (endNode === null) throw new Error("endNode does not exist in the grid");
@@ -36,7 +47,7 @@ export const findPath = (
addedNodes: boolean[][],
) => {
const neighbor: DirectionNode = nodes[node.y][node.x];
- let children = [] as PointInterface[];
+ const children = [] as PointInterface[];
if (neighbor === null || neighbor === undefined) return children;
possibleDirections.forEach((direction) => {
@@ -69,13 +80,14 @@ export const findPath = (
let done = false;
- const getEmptyArrayFromSize = (fill: any) =>
- Array(height)
+ function getEmptyArrayFromSize(fill: T): T[][] {
+ return Array(height)
.fill(0)
.map(() => Array(width).fill(fill));
+ }
- const addedNodes = getEmptyArrayFromSize(false) as boolean[][];
- const visitedNodes = getEmptyArrayFromSize(false) as boolean[][];
+ const addedNodes = getEmptyArrayFromSize(false);
+ const visitedNodes = getEmptyArrayFromSize(false);
const parents = getEmptyArrayFromSize(null) as (PointInterface | null)[][];
let queue = [startPoint] as PointInterface[];
@@ -86,7 +98,7 @@ export const findPath = (
visitedNodes[currentNode.y][currentNode.x] = true;
const children = getChildren(
- grid.nodes as DirectionNode[][],
+ nodes as DirectionNode[][],
currentNode,
addedNodes,
) as PointInterface[];
@@ -103,7 +115,7 @@ export const findPath = (
// We basically get the path backwards once we create found the node
- let end: PointInterface = endPoint.copy(0, 0);
+ let end: PointInterface = new Point(endPoint.x, endPoint.y);
const steps = [] as PointInterface[];
while (end.x !== startPoint.x || end.y !== startPoint.y) {
steps.push(end);
@@ -111,3 +123,65 @@ export const findPath = (
}
return [...steps, startPoint].reverse();
};
+
+const buildNodes = (
+ grid: Grid,
+ maxJumpCost: number,
+): (DirectionNode | null)[][] => {
+ return grid.mapMatrix((x, y, cost) => {
+ if (cost === null) return null;
+
+ const directionNode = new DirectionNode(new Point(x, y));
+
+ const assignDirectionNodeIf = (comparison: boolean, point: Point) => {
+ if (!comparison) return null;
+ const nodePoint = new Point(
+ directionNode.point.x + point.x,
+ directionNode.point.y + point.y,
+ );
+ const neighborCost = grid.getHeightAt(nodePoint);
+
+ return neighborCost !== null &&
+ Math.abs(neighborCost - cost) <= maxJumpCost
+ ? nodePoint
+ : null;
+ };
+
+ directionNode.northNode = assignDirectionNodeIf(
+ y - 1 >= 0 && y - 1 < grid.height,
+ new Point(0, -1),
+ );
+ directionNode.southNode = assignDirectionNodeIf(
+ y + 1 >= 0 && y + 1 < grid.height,
+ new Point(0, 1),
+ );
+ directionNode.westNode = assignDirectionNodeIf(
+ x - 1 >= 0 && x - 1 < grid.width,
+ new Point(-1, 0),
+ );
+ directionNode.eastNode = assignDirectionNodeIf(
+ x + 1 >= 0 && x + 1 < grid.width,
+ new Point(1, 0),
+ );
+
+ // Add diagonals
+ directionNode.northWestNode = assignDirectionNodeIf(
+ y - 1 >= 0 && x - 1 >= 0,
+ new Point(-1, -1),
+ );
+ directionNode.northEastNode = assignDirectionNodeIf(
+ y - 1 >= 0 && x + 1 < grid.width,
+ new Point(1, -1),
+ );
+ directionNode.southWestNode = assignDirectionNodeIf(
+ y + 1 < grid.height && x - 1 >= 0,
+ new Point(-1, 1),
+ );
+ directionNode.southEastNode = assignDirectionNodeIf(
+ y + 1 < grid.height && x + 1 < grid.width,
+ new Point(1, 1),
+ );
+
+ return directionNode;
+ });
+};
diff --git a/src/objects/grid.ts b/src/objects/grid.ts
index 4ba704a..d9eadcd 100644
--- a/src/objects/grid.ts
+++ b/src/objects/grid.ts
@@ -1,120 +1,131 @@
-import { Node } from "./nodes/node.ts";
import { Point } from "./point/point.ts";
-import { findPath } from "../finders/jumpPoint.finder.ts";
+import { findPath as jumpPointFindPath } from "../finders/jumpPoint.finder.ts";
+import { findPath as customFindPath } from "../finders/custom.finder.ts";
import { PointInterface } from "./point/point.interface.ts";
import { FinderEnum } from "../finders/finder.enum.ts";
-import { DirectionNode } from "./nodes/directionNode.ts";
import { makeSquare } from "../utils/grid.utils.ts";
+import { FindPathConfig } from "../finders/finder.types.ts";
+
+const NON_WALKABLE_HEIGHT = 0;
export class Grid {
- private readonly _matrix: number[][];
- public nodes: Node[][] | (DirectionNode | null)[][];
+ public readonly width: number;
+ public readonly height: number;
+ private readonly heightMatrix: Float32Array;
+
+ constructor(width: number, height: number, costMatrix: Float32Array) {
+ this.width = width;
+ this.height = height;
+ this.heightMatrix = costMatrix;
- constructor(matrix: number[][]) {
- this._matrix = makeSquare(matrix);
+ if (width !== height) throw new Error("grid matrix must be square");
- if (this._matrix[0] === undefined || this._matrix[0][0] === undefined)
+ if (costMatrix.length !== width * height)
+ throw new Error("cost matrix must have the same dimensions as the grid");
+ }
+
+ public static from(matrix: number[][]): Grid {
+ if (matrix[0] === undefined || matrix[0][0] === undefined)
throw new Error("grid matrix cannot be empty");
- this.nodes = [];
+ const mat = makeSquare(matrix);
+ const height = mat.length;
+ const width = mat[0].length;
+
+ if (height !== width) throw new Error("grid matrix must be square");
+
+ const costMatrix = new Float32Array(height * width);
+ mat.forEach((row, y) => {
+ row.forEach((cost, x) => {
+ costMatrix[y * width + x] = cost;
+ });
+ });
+
+ return new Grid(width, height, costMatrix);
}
- private buildNodes(maxJumpCost: number) {
- this.nodes = this._matrix.map((arrY, y) =>
- arrY.map((cost, x) => {
- if (cost === null) return null;
-
- const directionNode = new DirectionNode(new Point(x, y));
-
- const assignDirectionNodeIf = (comparison: boolean, point: Point) => {
- if (!comparison) return null;
- const nodePoint = directionNode.point.copy(point.x, point.y);
- const neighborCost = this.getMatrixCost(nodePoint);
-
- return neighborCost !== null &&
- Math.abs(neighborCost - cost) <= maxJumpCost
- ? nodePoint
- : null;
- };
-
- directionNode.northNode = assignDirectionNodeIf(
- y - 1 >= 0 && y - 1 < this.height,
- new Point(0, -1),
- );
- directionNode.southNode = assignDirectionNodeIf(
- y + 1 >= 0 && y + 1 < this.height,
- new Point(0, 1),
- );
- directionNode.westNode = assignDirectionNodeIf(
- x - 1 >= 0 && x - 1 < this.width,
- new Point(-1, 0),
- );
- directionNode.eastNode = assignDirectionNodeIf(
- x + 1 >= 0 && x + 1 < this.width,
- new Point(1, 0),
- );
-
- // Add diagonals
- directionNode.northWestNode = assignDirectionNodeIf(
- y - 1 >= 0 && x - 1 >= 0,
- new Point(-1, -1),
- );
- directionNode.northEastNode = assignDirectionNodeIf(
- y - 1 >= 0 && x + 1 < this.width,
- new Point(1, -1),
- );
- directionNode.southWestNode = assignDirectionNodeIf(
- y + 1 < this.height && x - 1 >= 0,
- new Point(-1, 1),
- );
- directionNode.southEastNode = assignDirectionNodeIf(
- y + 1 < this.height && x + 1 < this.width,
- new Point(1, 1),
- );
-
- return directionNode;
- }),
- );
+ public getHeightAt(point: PointInterface): number | null {
+ if (!this.inBounds(point)) return null;
+ const cost = this.heightMatrix[this.index(point)];
+ return cost === NON_WALKABLE_HEIGHT ? null : cost;
}
- public getNode(point: Point): Node | DirectionNode | null {
- if (this.nodes[point.y] === undefined) return null;
- return this.nodes[point.y][point.x];
+ public distance(a: PointInterface, b: PointInterface): number {
+ // return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
+ return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}
- public getMatrixCost(point: Point): number | null {
- if (this._matrix[point.y] === undefined) return null;
- return this._matrix[point.y][point.x];
+ public index(point: PointInterface): number {
+ return point.y * this.height + point.x;
}
private clone(): Grid {
- return new Grid(this._matrix);
+ return new Grid(
+ this.width,
+ this.height,
+ new Float32Array(this.heightMatrix),
+ );
}
- get width(): number {
- return this._matrix[0].length;
+ public inBounds(point: PointInterface): boolean {
+ return (
+ point.x >= 0 &&
+ point.x < this.width &&
+ point.y >= 0 &&
+ point.y < this.height
+ );
}
- get height(): number {
- return this._matrix.length;
+ public isWalkable(point: PointInterface): boolean {
+ const heightAt = this.getHeightAt(point);
+ return (
+ this.inBounds(point) &&
+ heightAt !== null &&
+ heightAt !== NON_WALKABLE_HEIGHT
+ );
+ }
+
+ public walkMatrix(
+ callback: (x: number, y: number, cost: number | null) => void,
+ ) {
+ for (let y = 0; y < this.height; y++) {
+ for (let x = 0; x < this.width; x++) {
+ callback(x, y, this.heightMatrix[y * this.height + x]);
+ }
+ }
+ }
+
+ public mapMatrix(
+ callback: (x: number, y: number, cost: number | null) => T,
+ ): T[][] {
+ const matrix: T[][] = [];
+
+ for (let y = 0; y < this.height; y++) {
+ const row: T[] = [];
+ for (let x = 0; x < this.width; x++) {
+ const value: T = callback(x, y, this.heightMatrix[y * this.height + x]);
+ row.push(value);
+ }
+ matrix.push(row);
+ }
+
+ return matrix;
}
public findPath(
startPoint: PointInterface,
endPoint: PointInterface,
- maxJumpCost = 5,
- finderEnum: FinderEnum = FinderEnum.JUMP_POINT,
+ config: FindPathConfig = {
+ finder: FinderEnum.CUSTOM,
+ maxJumpCost: 5,
+ travelCosts: undefined,
+ },
): PointInterface[] {
- const gridClone = this.clone();
- gridClone.buildNodes(maxJumpCost);
-
- switch (finderEnum) {
+ switch (config.finder) {
case FinderEnum.JUMP_POINT:
- return findPath(
- new Point(startPoint.x, startPoint.y),
- new Point(endPoint.x, endPoint.y),
- gridClone,
- );
+ return jumpPointFindPath(startPoint, endPoint, this, config);
+ case FinderEnum.CUSTOM:
+ return customFindPath(startPoint, endPoint, this, config);
}
}
}
diff --git a/src/objects/openList.ts b/src/objects/openList.ts
index b543aed..bb6cbb9 100644
--- a/src/objects/openList.ts
+++ b/src/objects/openList.ts
@@ -2,21 +2,25 @@ import { CompFn, ListNode } from "./openList.types.ts";
export class OpenList {
private start: ListNode | null;
+ private size: number;
private readonly comparator: CompFn;
constructor(comparator: CompFn) {
this.start = null;
+ this.size = 0;
this.comparator = comparator;
}
push(value: T) {
if (this.start === null) {
this.start = { value, next: null };
+ this.size++;
return;
}
if (this.comparator(value, this.start.value) < 0) {
this.start = { value, next: this.start };
+ this.size++;
return;
}
@@ -26,16 +30,32 @@ export class OpenList {
}
aux.next = { value, next: aux.next };
+ this.size++;
}
pop(): T {
if (this.start === null) throw new Error("popping from an empty list");
const popped = this.start;
this.start = popped.next;
+ this.size--;
return popped.value;
}
- empty(): boolean {
+ length(): number {
+ return this.size;
+ }
+
+ isEmpty(): boolean {
return this.start === null;
}
+
+ toString(): string {
+ let aux: ListNode | null = this.start;
+ let str = "";
+ while (aux !== null) {
+ str += aux.value + " -> ";
+ aux = aux.next;
+ }
+ return str;
+ }
}
diff --git a/src/objects/point/point.ts b/src/objects/point/point.ts
index 8cb13d3..da27c63 100644
--- a/src/objects/point/point.ts
+++ b/src/objects/point/point.ts
@@ -16,4 +16,8 @@ export class Point implements PointInterface {
equal(point: Point): boolean {
return this.x === point.x && this.y === point.y;
}
+
+ toString(): string {
+ return `(${this.x}, ${this.y})`;
+ }
}
diff --git a/src/sandbox.ts b/src/sandbox.ts
index 3b173ae..c153bab 100644
--- a/src/sandbox.ts
+++ b/src/sandbox.ts
@@ -14,12 +14,15 @@ const original = [
const layout = transpose(original); // (original);
-const grid = new Grid(layout);
+const grid = Grid.from(layout);
const start = { x: 2, y: 6 };
const end = { x: 5, y: 9 };
console.log(start, "->", end);
-const path = grid.findPath(start, end, 1, FinderEnum.JUMP_POINT);
+const path = grid.findPath(start, end, {
+ finder: FinderEnum.JUMP_POINT,
+ maxJumpCost: 1,
+});
console.log(path);
drawLayout(layout, path, start, end);
diff --git a/src/web_sandbox.ts b/src/web_sandbox.ts
new file mode 100644
index 0000000..58a290c
--- /dev/null
+++ b/src/web_sandbox.ts
@@ -0,0 +1,179 @@
+import { Grid } from "./objects/grid.ts";
+import { FinderEnum } from "./finders/finder.enum.ts";
+import { transpose } from "./utils/grid.utils.ts";
+import { Point } from "./objects/point/point.ts";
+import { FindPathConfig } from "./finders/finder.types.ts";
+
+const handler = async (request: Request): Promise => {
+ // Load the start page if the user is accessing the root path
+ if (new URL(request.url).pathname === "/") {
+ const file = await Deno.readTextFile("./res/debug_view.html");
+ return new Response(file, {
+ status: 200,
+ headers: {
+ "content-type": "text/html",
+ },
+ });
+ }
+
+ const params = new URL(request.url).searchParams;
+
+ const original = [
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0,
+ ],
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0,
+ ],
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0,
+ ],
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0,
+ ],
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0,
+ ],
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0,
+ ],
+ [
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0,
+ ],
+ [
+ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 7, 10, 10, 10, 10, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 7, 10, 10, 10, 10, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 7, 10, 10, 10, 10, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 7, 10, 10, 10, 10, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 12, 12, 12, 12, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 15, 15, 15, 15, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ [
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19,
+ 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+ ],
+ ];
+
+ const layout = transpose(original);
+
+ const grid = Grid.from(layout);
+ const start = {
+ x: +(params.get("srcX") ?? "13"),
+ y: +(params.get("srcY") ?? "20"),
+ };
+ const end = {
+ x: +(params.get("dstX") ?? "15"),
+ y: +(params.get("dstY") ?? "20"),
+ };
+
+ const config: FindPathConfig = {
+ finder: FinderEnum.CUSTOM,
+ maxJumpCost: 4,
+ };
+ const path = grid.findPath(start, end, config);
+ const data = new Uint8Array(grid.width * grid.height);
+
+ for (let y = 0; y < grid.height; y++) {
+ for (let x = 0; x < grid.width; x++) {
+ data[y * grid.height + x] = grid.getHeightAt(new Point(x, y)) || 0;
+ }
+ }
+
+ const body = JSON.stringify({
+ path,
+ start,
+ end,
+ grid: {
+ data,
+ width: grid.width,
+ height: grid.height,
+ metadata: config.travelCosts,
+ metadata2: config.travelHeuristic,
+ },
+ });
+
+ return new Response(body, {
+ status: 200,
+ headers: {
+ "content-type": "application/json",
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
+ },
+ });
+};
+
+const port = 3000;
+console.log(`HTTP server running. Access it at: http://localhost:${port}/`);
+Deno.serve({ port }, handler);