Skip to content

Commit

Permalink
🎉 first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
fabienjuif committed Mar 28, 2020
0 parents commit 08eae34
Show file tree
Hide file tree
Showing 9 changed files with 4,985 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
*.log
.eslintcache
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"trailingComma": "all",
"semi": false,
"singleQuote": true,
"arrowParens": "always",
"endOfLine": "lf"
}
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Fabien JUIF

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# @fabienjuif/astar

> an A\* algorithm in javascript
## Installation

`npm install --save @fabienjuif/astar`

## Usage

```js
const getClosestPath = require('@fabienjuif/astar')

getClosestPath(
// array of rows that contains cells
graph,
// starting cell [x, y]
start,
// ending cell [x, y]
end,
// all the rest of parameters are **not required**
// but you can use them to tweak the engine
({
// function to test two node are the same
sameNode = defaultSameNode,
// function that map the given graph to the one that the engine needs
mapGraph = identity,
// function that map given cells to the ones that the engine needs
mapNode = identity,
// function to get neighbours of a cell (default works with squared cells)
getNeighbours = defaultGetNeighbours,
// function to get distance between 2 cells, default use pythagore (without the square root)
distance = defaultDistance,
// your heuristic
heuristic = () => 1,
// max loops before stoping the engine
maxLoops = Infinity,
} = {}),
)
```

- Node are representated in array `[x, y]`
- Graph is an array of cells

<!-- prettier-ignore -->
```js
const graph = [
// first line (x === 0)
[[0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [0, 5]],
// second line (x === 1)
[[1, 0], [1, 1], [1, 2]],
// third line (x === 2)
[[2, 0], [2, 1], [2, 2], [2, 3],[2, 4],[2, 5]],
]

// get cells at x = 2, y = 3
console.log(graph[2][3])
// > [2, 3]
```
113 changes: 113 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// https://fr.wikipedia.org/wiki/Algorithme_A*
// A* Search Algorithm

// node: [x, y, cost, h, parentNode]

function identity(o) {
return o
}
function sortNodes(node1, node2) {
if (node1[3] > node2[3]) return 1
if (node1[3] < node2[3]) return -1
return 0
}
function getFinalPath(end) {
if (!end[4]) return [end.slice(0, 3)]
return [...getFinalPath(end[4]), end.slice(0, 3)]
}
function defaultSameNode(node1, node2) {
return node1[0] === node2[0] && node1[1] === node2[1]
}

function defaultGetNeighbours(graph, node, { mapNode = identity } = {}) {
const neighbours = []

// left
let next = graph[node[0] - 1] && graph[node[0] - 1][node[1]]
if (next) neighbours.push(mapNode(next))

// right
next = graph[node[0] + 1] && graph[node[0] + 1][node[1]]
if (next) neighbours.push(mapNode(next))

// top
next = graph[node[0]][node[1] - 1]
if (next) neighbours.push(mapNode(next))

// bottom
next = graph[node[0]][node[1] + 1]
if (next) neighbours.push(mapNode(next))

return neighbours
}

function defaultDistance(node, end) {
const x = end[0] - node[0]
const y = end[1] - node[1]

return x * x + y * y
}

module.exports = function getClosestPath(
graph,
start,
end,
{
sameNode = defaultSameNode,
mapGraph = identity,
mapNode = identity,
getNeighbours = defaultGetNeighbours,
distance = defaultDistance,
heuristic = () => 1,
maxLoops = Infinity,
} = {},
) {
const mappedGraph = mapGraph(
[...graph].map((row) => [...row].map((cell) => [...cell])),
)
const closedList = []
const openList = []

openList.push(mapNode(start).concat(0))

let loop = -1
while (openList.length > 0 && loop++ < maxLoops) {
const current = openList.shift()

if (current[2] === Infinity) {
return [-2, [], loop]
}

if (sameNode(current, end)) {
return [0, getFinalPath(current), loop]
}

const neighbours = getNeighbours(mappedGraph, current, { mapNode })
for (let i = 0; i < neighbours.length; i += 1) {
const neighbour = neighbours[i]
const known = neighbour[2] !== undefined

if (closedList.find((n) => sameNode(n, neighbour))) continue

const newCost =
(current[2] || 0) +
heuristic(current.slice(0, 2), neighbour.slice(0, 2))

if (known && neighbour[2] < newCost) continue

neighbour[2] = newCost
neighbour[3] = neighbour[2] + distance(neighbour, end)
neighbour[4] = current
if (!known) openList.push(neighbour)
openList.sort(sortNodes)
}

closedList.push(current)
}

if (loop >= maxLoops) {
return [1, getFinalPath(openList[0]), loop]
}

return [-1, [], loop]
}
84 changes: 84 additions & 0 deletions index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* eslint-env jest */
const getClosestPath = require('./index')

const graph = [
[
[0, 0],
[0, 1],
[0, 2],
[0, 3],
[0, 4],
[0, 5],
],
[
[1, 0],
[1, 1],
[1, 2],
],
[
[2, 0],
[2, 1],
[2, 2],
[2, 3],
[2, 4],
[2, 5],
],
]

it('should find the closest path with all default', () => {
expect(getClosestPath(graph, [0, 0], [2, 3])).toEqual([
// route found
0,
// path
[
[0, 0, 0],
[0, 1, 1],
[1, 1, 2],
[1, 2, 3],
[2, 2, 4],
[2, 3, 5],
],
// loops
5,
])
})

it('should get the best path with the max loops exceed', () => {
expect(getClosestPath(graph, [0, 0], [2, 3], { maxLoops: 2 })).toEqual([
// hit max loops
1,
// path
[
[0, 0, 0],
[0, 1, 1],
[1, 1, 2],
[1, 2, 3],
],
// loops
3,
])
})

it('should not find a path', () => {
expect(getClosestPath(graph, [0, 0], [3, 3])).toEqual([
// path not found
-1,
// path
[],
// loops
14,
])
})

it('should not find a path because all nodes are walls', () => {
expect(
getClosestPath(graph, [0, 0], [2, 3], { heuristic: () => Infinity }),
).toEqual([
// path not found (blocked)
-2,
// path
[],
// loops
1,
])
})
67 changes: 67 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "@fabienjuif/astar",
"version": "0.1.0",
"main": "index.js",
"license": "MIT",
"homepage": "https://github.com/fabienjuif/astar",
"repository": "github:fabienjuif/astar",
"bugs": "https://github.com/fabienjuif/astar/issues",
"author": "Fabien JUIF <[email protected]>",
"scripts": {
"test": "jest",
"lint": "eslint --cache \"**/*.js\"",
"format": "prettier \"**/*.{ts,tsx,js,jsx,md,json}\" --write",
"ci:check": "prettier \"**/*.{ts,tsx,js,jsx,md,json}\" --check",
"ci": "run-p lint test ci:*"
},
"dependencies": {},
"keywords": [
"astar",
"a*",
"A",
"algogithm",
"video-game",
"game",
"shortest-path",
"shortest",
"path"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-prettier": "^3.1.2",
"husky": ">=4",
"jest": "^25.2.3",
"lint-staged": ">=10",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.2"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,css,md}": "prettier --write"
},
"eslintConfig": {
"extends": [
"airbnb-base",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"semi": "off",
"no-plusplus": "off",
"no-continue": "off"
}
}
}
23 changes: 23 additions & 0 deletions workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Quality

on: [push]

jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@v1
- name: Caching
uses: actions/cache@v1
with:
path: ~/.cache
key: cache-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
cache-${{ hashFiles('**/yarn.lock') }}
- name: Use Node.js
uses: actions/setup-node@v1
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Lint & Tests
run: yarn ci
Loading

0 comments on commit 08eae34

Please sign in to comment.