diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1ef95d..5cbc87f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,8 +5,8 @@ on: pull_request: jobs: - tests: - name: "tests" + prettier: + name: "prettier" runs-on: ubuntu-latest strategy: matrix: @@ -21,6 +21,9 @@ jobs: registry-url: "https://registry.npmjs.org" - run: yarn install --frozen-lockfile + - name: run prettier checker + run: yarn prettier:check + - name: run prettier checker run: yarn prettier:check @@ -28,5 +31,3 @@ jobs: uses: denoland/setup-deno@v1 with: deno-version: ${{ matrix.deno-version }} - - name: run tests - run: deno task test diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..38cfaca --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,42 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: './preview' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 06412bf..5efe72c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,11 @@ const grid = Grid.from([ #### PF example 1 ```ts -grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, {finder: FinderEnum.JUMP_POINT, maxJumpCost: 10}); +grid.findPath( + { x: 4, y: 2 }, + { x: 4, y: 1 }, + { finder: FinderEnum.JUMP_POINT, maxJumpCost: 10 }, +); ``` ```js @@ -40,7 +44,7 @@ grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, {finder: FinderEnum.JUMP_POINT, ma #### PF example 2 ```ts -grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, {maxJumpCost: 1}); +grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, { maxJumpCost: 1 }); ``` ```js @@ -50,7 +54,7 @@ grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, {maxJumpCost: 1}); #### PF example 3 ```ts -grid.findPath({ x: 4, y: 2 }, { x: 4, y: 1 }, {maxJumpCost: 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 deleted file mode 100644 index d320c16..0000000 --- a/__tests__/jumpPoint.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { smallGrid, testCasesSmallGrid } from "./test-data/small-grid.ts"; -import { testCasesBigGrid } from "./test-data/big-grid.ts"; -import {FinderEnum, Grid} from "../mod.ts"; - -import { assertEquals } from "std/assert/assert_equals.ts"; -import { describe, it } from "std/testing/bdd.ts"; -import { assertThrows } from "std/testing/asserts.ts"; - -describe("Diagonal Jump Point", () => { - const testCases = [...testCasesSmallGrid, ...testCasesBigGrid]; - - testCases.forEach( - ({ startPoint, endPoint, maxJumpCost, path: expectedPath, grid }) => { - it(`validates pathfinding from {${Object.values(startPoint)}} to {${Object.values(endPoint)}} with jumpCost {${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)); - }); - }, - ); - - it("throws an error if start point does no exist", () => { - 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 = 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 = () => Grid.from([]); - assertThrows(createEmptyGrid, Error, "grid matrix cannot be empty"); - }); -}); diff --git a/__tests__/openList.test.ts b/__tests__/openList.test.ts deleted file mode 100644 index b9e59e7..0000000 --- a/__tests__/openList.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { OpenList } from "../src/objects/openList.ts"; - -import { assertEquals } from "std/assert/assert_equals.ts"; -import { describe, it, beforeEach } from "std/testing/bdd.ts"; -import { assertThrows } from "std/testing/asserts.ts"; - -describe("OpenList", () => { - let list: OpenList; - - beforeEach(() => { - list = new OpenList((a, b) => a - b); - }); - - it("inserts one", () => { - list.push(1); - assertEquals(list.pop(), 1); - }); - - it("is empty", () => { - assertEquals(list.isEmpty(), true); - }); - - it("is not empty", () => { - list.push(1); - assertEquals(list.isEmpty(), false); - }); - - it("removes lowest value", () => { - list.push(3); - list.push(5); - list.push(1); - assertEquals(list.pop(), 1); - assertEquals(list.pop(), 3); - assertEquals(list.pop(), 5); - }); - - it("errors when popping from empty list", () => { - assertThrows(() => list.pop(), Error, "popping from an empty list"); - }); -}); diff --git a/__tests__/test-data/big-grid.ts b/__tests__/test-data/big-grid.ts deleted file mode 100644 index b001fa6..0000000 --- a/__tests__/test-data/big-grid.ts +++ /dev/null @@ -1,133 +0,0 @@ -const grid_B = [ - [ - 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, - ], -]; - -export const testCasesBigGrid = [ - { - grid: grid_B, - startPoint: { x: 15, y: 20 }, - endPoint: { x: 13, y: 20 }, - maxJumpCost: 4, - path: [ - { x: 15, y: 20 }, - { x: 15, y: 11 }, - { x: 4, y: 11 }, - { x: 13, y: 20 }, - ], - }, - { - grid: grid_B, - startPoint: { x: 13, y: 20 }, - endPoint: { x: 15, y: 20 }, - maxJumpCost: 4, - path: [ - { x: 13, y: 20 }, - { x: 3, y: 20 }, - { x: 15, y: 8 }, - { x: 15, y: 20 }, - ], - }, -]; diff --git a/__tests__/test-data/small-grid.ts b/__tests__/test-data/small-grid.ts deleted file mode 100644 index a9ab689..0000000 --- a/__tests__/test-data/small-grid.ts +++ /dev/null @@ -1,77 +0,0 @@ -export const smallGrid = [ - [50, 55, 60, 65, 70], - [45, 0, 0, 0, 75], - [40, 0, 0, 0, 1], - [35, 0, 0, 0, 5], - [30, 25, 20, 15, 10], -]; - -export const testCasesSmallGrid = [ - { - grid: smallGrid, - startPoint: { x: 4, y: 2 }, - endPoint: { x: 4, y: 1 }, - maxJumpCost: 5, - path: [ - { x: 4, y: 2 }, - { x: 4, y: 4 }, - { x: 0, y: 4 }, - { x: 0, y: 0 }, - { x: 4, y: 0 }, - { x: 4, y: 1 }, - ], - }, - { - grid: smallGrid, - startPoint: { x: 4, y: 2 }, - endPoint: { x: 4, y: 1 }, - maxJumpCost: 100, - path: [ - { x: 4, y: 2 }, - { x: 4, y: 1 }, - ], - }, - { - grid: smallGrid, - startPoint: { x: 4, y: 2 }, - endPoint: { x: 4, y: 1 }, - maxJumpCost: 1, - path: [], - }, - { - grid: smallGrid, - startPoint: { x: 2, y: 0 }, - endPoint: { x: 4, y: 4 }, - maxJumpCost: 10, - path: [ - { x: 2, y: 0 }, - { x: 0, y: 0 }, - { x: 0, y: 4 }, - { x: 4, y: 4 }, - ], - }, - { - grid: smallGrid, - startPoint: { x: 4, y: 3 }, - endPoint: { x: 0, y: 1 }, - maxJumpCost: 5, - path: [ - { x: 4, y: 3 }, - { x: 4, y: 4 }, - { x: 0, y: 4 }, - { x: 0, y: 1 }, - ], - }, - { - grid: smallGrid, - startPoint: { x: 0, y: 1 }, - endPoint: { x: 4, y: 3 }, - maxJumpCost: 5, - path: [ - { x: 0, y: 1 }, - { x: 0, y: 4 }, - { x: 4, y: 4 }, - { x: 4, y: 3 }, - ], - }, -]; diff --git a/deno.json b/deno.json index 5330c00..c5da76c 100644 --- a/deno.json +++ b/deno.json @@ -2,10 +2,6 @@ "name": "@oh/pathfinding", "version": "0.0.0", "exports": "./mod.ts", - "tasks": { - "start": "deno run -A ./src/sandbox.ts", - "test": "deno test --allow-none --coverage=coverage" - }, "imports": { "std/": "https://deno.land/std@0.224.0/" } diff --git a/package.json b/package.json index 9245077..bcb249f 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "scripts": { + "bundle": "esbuild --bundle mod.ts --outfile=./preview/bundle.js --format=esm", "prettier:check": "prettier -c ./", "prettier:write": "prettier --write ." }, "devDependencies": { - "prettier": "3.3.3" + "prettier": "3.3.3", + "esbuild": "0.23.0" } } diff --git a/preview/bundle.js b/preview/bundle.js new file mode 100644 index 0000000..2a2df80 --- /dev/null +++ b/preview/bundle.js @@ -0,0 +1,301 @@ +// src/utils/grid.utils.ts +var makeSquare = (layout) => { + const maxLength = Math.max(layout.length, ...layout.map((row) => row.length)); + const squareLayout = Array.from({ length: maxLength }, () => + Array(maxLength).fill(null), + ); + for (let i = 0; i < layout.length; i++) { + for (let j = 0; j < layout[i].length; j++) { + squareLayout[i][j] = layout[i][j]; + } + } + return squareLayout; +}; +var transpose = (matrix) => { + const maxCols = Math.max(...matrix.map((row) => row.length)); + const transposed = Array.from({ length: maxCols }, () => + Array(matrix.length).fill(null), + ); + for (let i = 0; i < matrix.length; i++) { + for (let j = 0; j < matrix[i].length; j++) { + transposed[j][i] = matrix[i][j]; + } + } + return transposed; +}; + +// src/grid/openList.ts +var OpenList = class { + constructor(comparator) { + this.start = null; + this.size = 0; + this.comparator = comparator; + } + push(value) { + 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; + } + let aux = this.start; + while (aux.next !== null && this.comparator(value, aux.next.value) > 0) { + aux = aux.next; + } + aux.next = { value, next: aux.next }; + this.size++; + } + pop() { + 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; + } + length() { + return this.size; + } + isEmpty() { + return this.start === null; + } + toString() { + let aux = this.start; + let str = ""; + while (aux !== null) { + str += aux.value + " -> "; + aux = aux.next; + } + return str; + } +}; + +// src/grid/finder.ts +var NOT_REACHED_COST = 999999; +var MAX_JUMP_HEIGHT = 5; +var PathNode = class { + constructor(point, from, cost, heuristicValue) { + this.from = from; + this.point = point; + this.cost = cost; + this.heuristicValue = heuristicValue; + } + toString() { + return `(${this.point.x}, ${this.point.y}, cost: ${this.cost}, heuristic: ${this.heuristicValue})`; + } +}; +var findPath = (startPoint, endPoint, grid, config) => { + const diagonalCostMultiplier = config.diagonalCostMultiplier ?? 1; + const orthogonalCostMultiplier = config.orthogonalCostMultiplier ?? 1; + const maxJumpCost = config.maxJumpCost ?? 5; + const maxIterations = config.maxIterations ?? 99999; + const index = (point) => { + return point.y * grid.height + point.x; + }; + const getMoveCostAt = (src, dst) => { + if (!grid.inBounds(src) || !grid.inBounds(dst)) return null; + const srcHeight = grid.getHeightAt(src) ?? NOT_REACHED_COST; + const dstHeight = grid.getHeightAt(dst) ?? NOT_REACHED_COST; + if (Math.abs(srcHeight - dstHeight) > MAX_JUMP_HEIGHT) return null; + return 1 + Math.abs(srcHeight - dstHeight); + }; + const addOrthogonalJumps = (prevNode, src, srcCost, dirX, dirY) => { + let jumpDistance = 1; + let accumulatedCost = 0; + let prevPoint = src; + while (true) { + const target = { + x: src.x + dirX * jumpDistance, + y: 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; + } + } + }; + const addDiagonal = (prevNode, src, srcCost, dirX, dirY) => { + const target = { x: src.x + dirX, y: src.y + dirY }; + const moveCost = + srcCost + + (getMoveCostAt(src, target) ?? NOT_REACHED_COST) * diagonalCostMultiplier; + const targetHeight = grid.getHeightAt(target); + const aux1 = { x: src.x, y: src.y + dirY }; + const aux2 = { x: src.x + dirX, y: 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; + const travelHeuristic = new Float32Array(grid.width * grid.height); + travelHeuristic.fill(NOT_REACHED_COST); + grid.walkMatrix((x, y) => { + travelHeuristic[y * grid.height + x] = grid.distance({ x, y }, endPoint); + }); + config.travelHeuristic = travelHeuristic; + const heuristic = (a) => { + return travelHeuristic[index(a)]; + }; + const comparator = (a, b) => { + return a.cost + a.heuristicValue - (b.cost + b.heuristicValue); + }; + const queue = 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 (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); + } + return []; +}; +var pathFromNode = (lastNode) => { + const path = []; + let node = lastNode; + while (node) { + path.push(node.point); + node = node.from; + } + return path.reverse(); +}; + +// src/grid/grid.ts +var NON_WALKABLE_HEIGHT = 0; +var Grid = class _Grid { + constructor(width, height, costMatrix) { + this.width = width; + this.height = height; + this.heightMatrix = costMatrix; + if (width !== height) throw new Error("grid matrix must be square"); + if (costMatrix.length !== width * height) + throw new Error("cost matrix must have the same dimensions as the grid"); + } + static from(matrix) { + if (matrix[0] === void 0 || matrix[0][0] === void 0) + throw new Error("grid matrix cannot be empty"); + 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); + } + getHeightAt(point) { + if (!this.inBounds(point)) return null; + const cost = this.heightMatrix[this.index(point)]; + return cost === NON_WALKABLE_HEIGHT ? null : cost; + } + distance(a, b) { + return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2); + } + index(point) { + return point.y * this.height + point.x; + } + clone() { + return new _Grid( + this.width, + this.height, + new Float32Array(this.heightMatrix), + ); + } + inBounds(point) { + return ( + point.x >= 0 && + point.x < this.width && + point.y >= 0 && + point.y < this.height + ); + } + isWalkable(point) { + const heightAt = this.getHeightAt(point); + return ( + this.inBounds(point) && + heightAt !== null && + heightAt !== NON_WALKABLE_HEIGHT + ); + } + walkMatrix(callback) { + 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]); + } + } + } + mapMatrix(callback) { + const matrix = []; + for (let y = 0; y < this.height; y++) { + const row = []; + for (let x = 0; x < this.width; x++) { + const value = callback(x, y, this.heightMatrix[y * this.height + x]); + row.push(value); + } + matrix.push(row); + } + return matrix; + } + findPath( + startPoint, + endPoint, + config = { + maxJumpCost: 5, + travelCosts: void 0, + }, + ) { + return findPath(startPoint, endPoint, this, config); + } +}; +export { Grid, makeSquare, transpose }; diff --git a/src/web_sandbox.ts b/preview/canvas.js similarity index 50% rename from src/web_sandbox.ts rename to preview/canvas.js index 58a290c..811a381 100644 --- a/src/web_sandbox.ts +++ b/preview/canvas.js @@ -1,23 +1,6 @@ -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; +import { Grid, transpose } from "./bundle.js"; +const getPathFinding = (start, end) => { 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, @@ -128,29 +111,18 @@ const handler = async (request: Request): Promise => { 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, + const config = { 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; - } - } + for (let y = 0; y < grid.height; y++) + for (let x = 0; x < grid.width; x++) + data[y * grid.height + x] = grid.getHeightAt({ x, y }) || 0; - const body = JSON.stringify({ + return { path, start, end, @@ -161,19 +133,153 @@ const handler = async (request: Request): Promise => { 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", - }, +/** @type {HTMLCanvasElement} */ +const canvas = document.querySelector("#main"); +const ctx = canvas.getContext("2d"); + +let start = { + x: 19, + y: 17, +}; + +let end = { + x: 19, + y: 12, +}; + +async function update() { + const data = getPathFinding(start, end); + window.data = data; + + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const cellSize = canvas.width / data.grid.width; + const metadata = data.grid.metadata ?? []; + const metadata2 = data.grid.metadata2 ?? []; + + const max = Object.values(data.grid.data).reduce( + (acc, val) => Math.max(acc, val), + 0, + ); + + for (let y = 0; y < data.grid.height; y++) { + for (let x = 0; x < data.grid.width; x++) { + const val = data.grid.data[y * data.grid.height + x]; + + if (val === 0) { + ctx.fillStyle = `black`; + } else { + ctx.fillStyle = `rgba(255, 255, 255, ${1 - (val / max) * 0.9})`; + } + ctx.fillRect(cellSize * x, cellSize * y, cellSize, cellSize); + ctx.strokeStyle = "#333"; + ctx.strokeRect(cellSize * x, cellSize * y, cellSize, cellSize); + + const height = metadata[y * data.grid.height + x] ?? 999999; + if (height < 999999) { + const pad = 1; + ctx.fillStyle = "#F00"; + ctx.font = "12px Arial"; + ctx.fillText( + String((height * 10) | 0), + cellSize * x + 2 + pad, + cellSize * y + 12 + pad, + ); + } + + const heuristic = metadata2[y * data.grid.height + x] ?? 999999; + if (heuristic < 999999) { + const pad = 1; + ctx.fillStyle = "#0F0"; + ctx.font = "12px Arial"; + ctx.fillText( + String((heuristic * 10) | 0), + cellSize * x + 2 + pad, + cellSize * y + 12 + pad + 10, + ); + } + } + } + + const pad = cellSize * 0.25; + ctx.fillStyle = "#0F0"; + ctx.fillRect( + cellSize * data.start.x + pad, + cellSize * data.start.y + pad, + cellSize - pad * 2, + cellSize - pad * 2, + ); + ctx.fillStyle = "#F00"; + ctx.fillRect( + cellSize * data.end.x + pad, + cellSize * data.end.y + pad, + cellSize - pad * 2, + cellSize - pad * 2, + ); + + let prevPoint = data.start; + data.path.forEach((point, i) => { + const pad = cellSize * 0.35; + + ctx.fillStyle = `rgba(0, 0, 255, ${i / data.path.length})`; + ctx.fillRect( + cellSize * point.x + pad, + cellSize * point.y + pad, + cellSize - pad * 2, + cellSize - pad * 2, + ); + + ctx.beginPath(); + ctx.moveTo( + cellSize * prevPoint.x + cellSize / 2, + cellSize * prevPoint.y + cellSize / 2, + ); + ctx.lineTo( + cellSize * point.x + cellSize / 2, + cellSize * point.y + cellSize / 2, + ); + ctx.closePath(); + let grad = ctx.createLinearGradient( + cellSize * prevPoint.x + cellSize / 2, + cellSize * prevPoint.y + cellSize / 2, + cellSize * point.x + cellSize / 2, + cellSize * point.y + cellSize / 2, + ); + grad.addColorStop(0, "#00F"); + grad.addColorStop(1, "#F0F"); + ctx.strokeStyle = grad; + ctx.lineWidth = 3; + ctx.stroke(); + ctx.lineWidth = 1; + + prevPoint = point; }); + + console.log("start", data.start); + console.log("end", data.end); + console.log("path", data.path); +} + +canvas.onmousedown = (e) => { + const cellSize = canvas.width / data.grid.width; + const x = Math.floor(e.offsetX / cellSize); + const y = Math.floor(e.offsetY / cellSize); + + if (e.button === 0) { + start = { x, y }; + } else { + end = { x, y }; + } + + update(); }; -const port = 3000; -console.log(`HTTP server running. Access it at: http://localhost:${port}/`); -Deno.serve({ port }, handler); +update(); + +window.oncontextmenu = function () { + return false; +}; diff --git a/preview/index.html b/preview/index.html new file mode 100644 index 0000000..6b3467e --- /dev/null +++ b/preview/index.html @@ -0,0 +1,32 @@ + + + + + + + + Pathfinder visualization + + + + + + + + diff --git a/res/debug_view.html b/res/debug_view.html deleted file mode 100644 index 6c3b9c3..0000000 --- a/res/debug_view.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - Pathfinder visualization - - - - - - - diff --git a/src/finders/finder.enum.ts b/src/finders/finder.enum.ts deleted file mode 100644 index bc0bc19..0000000 --- a/src/finders/finder.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum FinderEnum { - JUMP_POINT, - CUSTOM, -} diff --git a/src/finders/jumpPoint.finder.ts b/src/finders/jumpPoint.finder.ts deleted file mode 100644 index b37abf0..0000000 --- a/src/finders/jumpPoint.finder.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { Grid } from "../objects/grid.ts"; -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, - DirectionEnum.EAST, - DirectionEnum.SOUTH, - DirectionEnum.WEST, - DirectionEnum.NORTH_WEST, - DirectionEnum.NORTH_EAST, - DirectionEnum.SOUTH_WEST, - DirectionEnum.SOUTH_EAST, -]; - -export const findPath = ( - startPoint: PointInterface, - endPoint: PointInterface, - grid: Grid, - config: FindPathConfig, -): PointInterface[] => { - 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) { - throw new Error("startNode does not exist in the grid"); - } - - if (endNode === null) throw new Error("endNode does not exist in the grid"); - - const getChildren = ( - nodes: DirectionNode[][], - node: PointInterface, - addedNodes: boolean[][], - ) => { - const neighbor: DirectionNode = nodes[node.y][node.x]; - const children = [] as PointInterface[]; - if (neighbor === null || neighbor === undefined) return children; - - possibleDirections.forEach((direction) => { - let newNeighbor: Point | null = neighbor?.getNeighbor(direction) as Point; - if (newNeighbor === null) return; - - if (!addedNodes[newNeighbor.y][newNeighbor.x]) { - children.push(newNeighbor); - addedNodes[newNeighbor.y][newNeighbor.x] = true; - } - - // The rest - while ( - newNeighbor && - nodes[newNeighbor.y][newNeighbor.x]!.getNeighbor(direction) - ) { - newNeighbor = - nodes[newNeighbor.y][newNeighbor.x]!.getNeighbor(direction); - if (newNeighbor && !addedNodes[newNeighbor.y][newNeighbor.x]) { - children.push(newNeighbor); - addedNodes[newNeighbor.y][newNeighbor.x] = true; - } - } - }); - - return children; - }; - - const { width, height } = grid; - - let done = false; - - function getEmptyArrayFromSize(fill: T): T[][] { - return Array(height) - .fill(0) - .map(() => Array(width).fill(fill)); - } - - const addedNodes = getEmptyArrayFromSize(false); - const visitedNodes = getEmptyArrayFromSize(false); - const parents = getEmptyArrayFromSize(null) as (PointInterface | null)[][]; - - let queue = [startPoint] as PointInterface[]; - addedNodes[startPoint.y][startPoint.x] = true; - - while (!done && queue.length > 0) { - const currentNode = queue.shift() as PointInterface; - visitedNodes[currentNode.y][currentNode.x] = true; - - const children = getChildren( - nodes as DirectionNode[][], - currentNode, - addedNodes, - ) as PointInterface[]; - for (const { x, y } of children) { - parents[y][x] = currentNode; - - if (x === endPoint.x && y === endPoint.y) done = true; - } - - queue = [...queue, ...children]; - } - - if (!done) return []; - - // We basically get the path backwards once we create found the node - - 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); - end = parents[end.y][end.x]!; - } - 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/finders/custom.finder.ts b/src/grid/finder.ts similarity index 80% rename from src/finders/custom.finder.ts rename to src/grid/finder.ts index cdd03cc..67a0378 100644 --- a/src/finders/custom.finder.ts +++ b/src/grid/finder.ts @@ -1,20 +1,18 @@ -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"; +import { Point, FindPathConfig } from "../types/main.ts"; +import { Grid } from "./grid.ts"; +import { OpenList } from "./openList.ts"; const NOT_REACHED_COST: number = 999999; const MAX_JUMP_HEIGHT: number = 5; class PathNode { public from: PathNode | null; - public point: PointInterface; + public point: Point; public cost: number; public heuristicValue: number; constructor( - point: PointInterface, + point: Point, from: PathNode | null, cost: number, heuristicValue: number, @@ -31,24 +29,21 @@ class PathNode { } export const findPath = ( - startPoint: PointInterface, - endPoint: PointInterface, + startPoint: Point, + endPoint: Point, grid: Grid, config: FindPathConfig, -): PointInterface[] => { +): Point[] => { 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 => { + const index = (point: Point): number => { return point.y * grid.height + point.x; }; - const getMoveCostAt = ( - src: PointInterface, - dst: PointInterface, - ): number | null => { + const getMoveCostAt = (src: Point, dst: Point): number | null => { if (!grid.inBounds(src) || !grid.inBounds(dst)) return null; // Difference in height @@ -64,7 +59,7 @@ export const findPath = ( // Orthogonal jumps from JumpPoint const addOrthogonalJumps = ( prevNode: PathNode, - src: PointInterface, + src: Point, srcCost: number, dirX: number, dirY: number, @@ -74,10 +69,10 @@ export const findPath = ( let prevPoint = src; while (true) { - const target = new Point( - src.x + dirX * jumpDistance, - src.y + dirY * jumpDistance, - ); + const target: Point = { + x: src.x + dirX * jumpDistance, + y: src.y + dirY * jumpDistance, + }; if (!grid.isWalkable(target)) { break; @@ -110,18 +105,18 @@ export const findPath = ( // Diagonal movements with filter for adjacent walkable tiles in the same floor height const addDiagonal = ( prevNode: PathNode, - src: PointInterface, + src: Point, srcCost: number, dirX: number, dirY: number, ) => { - const target = new Point(src.x + dirX, src.y + dirY); + const target: Point = { x: src.x + dirX, y: 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 aux1: Point = { x: src.x, y: src.y + dirY }; + const aux2: Point = { x: src.x + dirX, y: src.y }; const targetIndex = index(target); if ( @@ -150,14 +145,11 @@ export const findPath = ( travelHeuristic.fill(NOT_REACHED_COST); grid.walkMatrix((x, y) => { - travelHeuristic[y * grid.height + x] = grid.distance( - new Point(x, y), - endPoint, - ); + travelHeuristic[y * grid.height + x] = grid.distance({ x, y }, endPoint); }); config.travelHeuristic = travelHeuristic; - const heuristic = (a: PointInterface): number => { + const heuristic = (a: Point): number => { return travelHeuristic[index(a)]; }; @@ -200,8 +192,8 @@ export const findPath = ( return []; }; -const pathFromNode = (lastNode: PathNode): PointInterface[] => { - const path: PointInterface[] = []; +const pathFromNode = (lastNode: PathNode): Point[] => { + const path: Point[] = []; let node: PathNode | null = lastNode; while (node) { path.push(node.point); diff --git a/src/objects/grid.ts b/src/grid/grid.ts similarity index 72% rename from src/objects/grid.ts rename to src/grid/grid.ts index d9eadcd..1acbc08 100644 --- a/src/objects/grid.ts +++ b/src/grid/grid.ts @@ -1,10 +1,6 @@ -import { Point } from "./point/point.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 { makeSquare } from "../utils/grid.utils.ts"; -import { FindPathConfig } from "../finders/finder.types.ts"; +import { FindPathConfig, Point } from "../types/main.ts"; +import { findPath } from "./finder.ts"; const NON_WALKABLE_HEIGHT = 0; @@ -44,18 +40,18 @@ export class Grid { return new Grid(width, height, costMatrix); } - public getHeightAt(point: PointInterface): number | null { + public getHeightAt(point: Point): number | null { if (!this.inBounds(point)) return null; const cost = this.heightMatrix[this.index(point)]; return cost === NON_WALKABLE_HEIGHT ? null : cost; } - public distance(a: PointInterface, b: PointInterface): number { + public distance(a: Point, b: Point): 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 index(point: PointInterface): number { + public index(point: Point): number { return point.y * this.height + point.x; } @@ -67,7 +63,7 @@ export class Grid { ); } - public inBounds(point: PointInterface): boolean { + public inBounds(point: Point): boolean { return ( point.x >= 0 && point.x < this.width && @@ -76,7 +72,7 @@ export class Grid { ); } - public isWalkable(point: PointInterface): boolean { + public isWalkable(point: Point): boolean { const heightAt = this.getHeightAt(point); return ( this.inBounds(point) && @@ -113,19 +109,13 @@ export class Grid { } public findPath( - startPoint: PointInterface, - endPoint: PointInterface, + startPoint: Point, + endPoint: Point, config: FindPathConfig = { - finder: FinderEnum.CUSTOM, maxJumpCost: 5, travelCosts: undefined, }, - ): PointInterface[] { - switch (config.finder) { - case FinderEnum.JUMP_POINT: - return jumpPointFindPath(startPoint, endPoint, this, config); - case FinderEnum.CUSTOM: - return customFindPath(startPoint, endPoint, this, config); - } + ): Point[] { + return findPath(startPoint, endPoint, this, config); } } diff --git a/src/grid/main.ts b/src/grid/main.ts new file mode 100644 index 0000000..64d23cb --- /dev/null +++ b/src/grid/main.ts @@ -0,0 +1 @@ +export * from "./grid.ts"; diff --git a/src/objects/openList.ts b/src/grid/openList.ts similarity index 95% rename from src/objects/openList.ts rename to src/grid/openList.ts index bb6cbb9..c33a3c7 100644 --- a/src/objects/openList.ts +++ b/src/grid/openList.ts @@ -1,4 +1,4 @@ -import { CompFn, ListNode } from "./openList.types.ts"; +import { CompFn, ListNode } from "../types/main.ts"; export class OpenList { private start: ListNode | null; diff --git a/src/main.ts b/src/main.ts index 8e11f1e..1291a4b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,3 @@ -export * from "./objects/grid.ts"; -export * from "./finders/finder.enum.ts"; -export { transpose } from "./utils/grid.utils.ts"; +export * from "./grid/grid.ts"; +export * from "./types/finder.types.ts"; +export * from "./utils/grid.utils.ts"; diff --git a/src/objects/nodes/direction.enum.ts b/src/objects/nodes/direction.enum.ts deleted file mode 100644 index 4136fc1..0000000 --- a/src/objects/nodes/direction.enum.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum DirectionEnum { - NORTH, - EAST, - SOUTH, - WEST, - NORTH_WEST, - NORTH_EAST, - SOUTH_WEST, - SOUTH_EAST, -} diff --git a/src/objects/nodes/directionNode.ts b/src/objects/nodes/directionNode.ts deleted file mode 100644 index a37161d..0000000 --- a/src/objects/nodes/directionNode.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Point } from "../point/point.ts"; -import { DirectionEnum } from "./direction.enum.ts"; - -export class DirectionNode { - public readonly point: Point; - public northNode: Point | null; - public eastNode: Point | null; - public southNode: Point | null; - public westNode: Point | null; - public northWestNode: Point | null; - public northEastNode: Point | null; - public southWestNode: Point | null; - public southEastNode: Point | null; - - constructor( - point: Point, - northNode: Point | null = null, - eastNode: Point | null = null, - southNode: Point | null = null, - westNode: Point | null = null, - northWestNode: Point | null = null, - northEastNode: Point | null = null, - southWestNode: Point | null = null, - southEastNode: Point | null = null, - ) { - this.point = point; - this.northNode = northNode; - this.eastNode = eastNode; - this.southNode = southNode; - this.westNode = westNode; - this.northWestNode = northWestNode; - this.northEastNode = northEastNode; - this.southWestNode = southWestNode; - this.southEastNode = southEastNode; - } - - getNeighbor(direction: DirectionEnum): Point | null { - switch (direction) { - case DirectionEnum.NORTH: - return this.northNode; - case DirectionEnum.EAST: - return this.eastNode; - case DirectionEnum.SOUTH: - return this.southNode; - case DirectionEnum.WEST: - return this.westNode; - case DirectionEnum.NORTH_WEST: - return this.northWestNode; - case DirectionEnum.NORTH_EAST: - return this.northEastNode; - case DirectionEnum.SOUTH_WEST: - return this.southWestNode; - case DirectionEnum.SOUTH_EAST: - return this.southEastNode; - } - } -} diff --git a/src/objects/nodes/node.ts b/src/objects/nodes/node.ts deleted file mode 100644 index ca31d72..0000000 --- a/src/objects/nodes/node.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Point } from "../point/point.ts"; - -// Based on A* Node -export class Node { - public readonly point: Point; - public cost: number; - - public distanceFromStart: number; //g - public heuristicDistance: number; //h - public totalCost: number; //f - - public opened: boolean; - public closed: boolean; - - public parent: Node | undefined; - - constructor(point: Point, cost: number) { - this.point = point; - this.cost = cost; - - this.opened = false; - this.closed = false; - - this.distanceFromStart = 0; - this.heuristicDistance = 0; - this.totalCost = 0; - } -} diff --git a/src/objects/point/point.interface.ts b/src/objects/point/point.interface.ts deleted file mode 100644 index 95050a5..0000000 --- a/src/objects/point/point.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface PointInterface { - x: number; - y: number; -} diff --git a/src/objects/point/point.ts b/src/objects/point/point.ts deleted file mode 100644 index da27c63..0000000 --- a/src/objects/point/point.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { PointInterface } from "./point.interface.ts"; - -export class Point implements PointInterface { - public x: number; - public y: number; - - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } - - copy(x: number, y: number): Point { - return new Point(this.x + x, this.y + y); - } - - 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 deleted file mode 100644 index c153bab..0000000 --- a/src/sandbox.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Grid } from "./objects/grid.ts"; -import { FinderEnum } from "./finders/finder.enum.ts"; -import { drawLayout, transpose } from "./utils/grid.utils.ts"; - -const original = [ - [0, 0, 0, 2, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 0, 1, 1], - [0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 1, 1, 1, 1, 1, 1, 1, 1], - [2, 1, 1, 1, 1, 1], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 1, 1, 1, 1, 1, 1, 0, 1, 1], -]; - -const layout = transpose(original); // (original); - -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, { - finder: FinderEnum.JUMP_POINT, - maxJumpCost: 1, -}); -console.log(path); - -drawLayout(layout, path, start, end); diff --git a/src/finders/finder.types.ts b/src/types/finder.types.ts similarity index 66% rename from src/finders/finder.types.ts rename to src/types/finder.types.ts index 5371315..8ba84cb 100644 --- a/src/finders/finder.types.ts +++ b/src/types/finder.types.ts @@ -1,12 +1,5 @@ -import { FinderEnum } from "./finder.enum.ts"; - export type FindPathConfig = { - finder: FinderEnum; - - // JumpPoint Finder maxJumpCost?: number; - - // Custom Finder orthogonalCostMultiplier?: number; diagonalCostMultiplier?: number; maxIterations?: number; diff --git a/src/types/main.ts b/src/types/main.ts new file mode 100644 index 0000000..4b2df73 --- /dev/null +++ b/src/types/main.ts @@ -0,0 +1,3 @@ +export * from "./finder.types.ts"; +export * from "./openList.types.ts"; +export * from "./point.types.ts"; diff --git a/src/objects/openList.types.ts b/src/types/openList.types.ts similarity index 100% rename from src/objects/openList.types.ts rename to src/types/openList.types.ts diff --git a/src/types/point.types.ts b/src/types/point.types.ts new file mode 100644 index 0000000..6522523 --- /dev/null +++ b/src/types/point.types.ts @@ -0,0 +1,4 @@ +export type Point = { + x: number; + y: number; +}; diff --git a/src/utils/grid.utils.ts b/src/utils/grid.utils.ts index e10ade5..a904eb3 100644 --- a/src/utils/grid.utils.ts +++ b/src/utils/grid.utils.ts @@ -1,5 +1,3 @@ -import { PointInterface } from "../objects/point/point.interface.ts"; - export const makeSquare = (layout: number[][]): number[][] => { const maxLength = Math.max(layout.length, ...layout.map((row) => row.length)); const squareLayout = Array.from({ length: maxLength }, () => @@ -15,25 +13,6 @@ export const makeSquare = (layout: number[][]): number[][] => { return squareLayout; }; -export const drawLayout = ( - layout: number[][], - path: PointInterface[], - start?: PointInterface, - end?: PointInterface, -) => { - const map: any[] = layout.map((row) => row.map((cell) => (cell ? "." : "#"))); - - path.forEach((point) => { - map[point.y][point.x] = "P"; - }); - - if (start) map[start.y][start.x] = "S"; - if (end) map[end.y][end.x] = "X"; - - const mapString = map.map((row) => row.join(" ")).join("\n"); - console.log(mapString); -}; - export const transpose = (matrix: number[][]): number[][] => { const maxCols = Math.max(...matrix.map((row) => row.length)); const transposed: number[][] = Array.from({ length: maxCols }, () => diff --git a/yarn.lock b/yarn.lock index d89feca..3637479 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,156 @@ # yarn lockfile v1 +"@esbuild/aix-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259" + integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ== + +"@esbuild/android-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832" + integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ== + +"@esbuild/android-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99" + integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g== + +"@esbuild/android-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6" + integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ== + +"@esbuild/darwin-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e" + integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow== + +"@esbuild/darwin-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c" + integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ== + +"@esbuild/freebsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4" + integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw== + +"@esbuild/freebsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d" + integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ== + +"@esbuild/linux-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a" + integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw== + +"@esbuild/linux-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad" + integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw== + +"@esbuild/linux-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238" + integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA== + +"@esbuild/linux-loong64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280" + integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A== + +"@esbuild/linux-mips64el@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5" + integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w== + +"@esbuild/linux-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6" + integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw== + +"@esbuild/linux-riscv64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780" + integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw== + +"@esbuild/linux-s390x@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8" + integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg== + +"@esbuild/linux-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910" + integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ== + +"@esbuild/netbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c" + integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw== + +"@esbuild/openbsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db" + integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ== + +"@esbuild/openbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8" + integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg== + +"@esbuild/sunos-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8" + integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA== + +"@esbuild/win32-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d" + integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ== + +"@esbuild/win32-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7" + integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA== + +"@esbuild/win32-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" + integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== + +esbuild@0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.0.tgz#de06002d48424d9fdb7eb52dbe8e95927f852599" + integrity sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.23.0" + "@esbuild/android-arm" "0.23.0" + "@esbuild/android-arm64" "0.23.0" + "@esbuild/android-x64" "0.23.0" + "@esbuild/darwin-arm64" "0.23.0" + "@esbuild/darwin-x64" "0.23.0" + "@esbuild/freebsd-arm64" "0.23.0" + "@esbuild/freebsd-x64" "0.23.0" + "@esbuild/linux-arm" "0.23.0" + "@esbuild/linux-arm64" "0.23.0" + "@esbuild/linux-ia32" "0.23.0" + "@esbuild/linux-loong64" "0.23.0" + "@esbuild/linux-mips64el" "0.23.0" + "@esbuild/linux-ppc64" "0.23.0" + "@esbuild/linux-riscv64" "0.23.0" + "@esbuild/linux-s390x" "0.23.0" + "@esbuild/linux-x64" "0.23.0" + "@esbuild/netbsd-x64" "0.23.0" + "@esbuild/openbsd-arm64" "0.23.0" + "@esbuild/openbsd-x64" "0.23.0" + "@esbuild/sunos-x64" "0.23.0" + "@esbuild/win32-arm64" "0.23.0" + "@esbuild/win32-ia32" "0.23.0" + "@esbuild/win32-x64" "0.23.0" + prettier@3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"