From 5ba02a6f71b610e6bd7a17845d6453a662b6c30b Mon Sep 17 00:00:00 2001 From: cout970 Date: Fri, 26 Jul 2024 16:55:31 +0200 Subject: [PATCH 1/2] Add custom pathfinder with: - Orthogonal jumps, with max jump length - Diagonal jumps with checks for stairs and holes - Max jump height - Weights for orthogonal and diagonal approaches - Distance to end heuristic for better performance - Refactor grid to use native and compact Float32Array - Configurations options: see FindPathConfig - Simple Done.serve() with HTML canvas for visualization of grid and paths --- __tests__/jumpPoint.test.ts | 16 +-- __tests__/openList.test.ts | 4 +- res/debug_view.html | 170 +++++++++++++++++++++++++ src/finders/custom.finder.ts | 211 ++++++++++++++++++++++++++++++++ src/finders/finder.enum.ts | 1 + src/finders/finder.types.ts | 15 +++ src/finders/jumpPoint.finder.ts | 102 ++++++++++++--- src/objects/grid.ts | 185 +++++++++++++++------------- src/objects/openList.ts | 22 +++- src/objects/point/point.ts | 4 + src/sandbox.ts | 7 +- src/web_sandbox.ts | 179 +++++++++++++++++++++++++++ 12 files changed, 802 insertions(+), 114 deletions(-) create mode 100644 res/debug_view.html create mode 100644 src/finders/custom.finder.ts create mode 100644 src/finders/finder.types.ts create mode 100644 src/web_sandbox.ts 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); From 417c8969e91bd03893fd1b357212129d2804c4a7 Mon Sep 17 00:00:00 2001 From: cout970 Date: Fri, 26 Jul 2024 17:03:29 +0200 Subject: [PATCH 2/2] Update Readme.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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