Skip to content

Commit

Permalink
refactor A* logic; imrpove search times
Browse files Browse the repository at this point in the history
  • Loading branch information
Rodhlann committed Apr 29, 2024
1 parent a0bfafd commit 62ef638
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 101 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.DS_Store
111 changes: 52 additions & 59 deletions a-star.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,83 @@
class Node {
pos
parent // successor parent (optional)
x // x pos
y // y pos
f // sum of _g and _h
g // movement cost to move from the starting point to a point on the grid
h // estimated movement cost of moving from a point on the grid to the end point (heuristic)
p // successor parent (optional)
}

const DIRECTIONS = {
N: { x: 0, y: -1 },
NE: { x: 1, y: -1 },
// NE: { x: 1, y: -1 },
E: { x: 1, y: 0 },
SE: { x: 1, y: 1 },
S: { x: 0, y: 1 },
SW: { x: -1, y: 1 },
// SE: { x: 1, y: 1 },
S: { x: 0, y: 1 },
// SW: { x: -1, y: 1 },
W: { x: -1, y: 0 },
NW: { x: -1, y: -1 },
// NW: { x: -1, y: -1 },
}

function isValid(x, y, closed, grid) {
return (
!(x > grid[0].length-1) && // not outside grid boundary
x > -1 &&
!(y > grid.length-1) &&
y > -1 &&
grid[y][x] !== 1 && // Not collision
!(closed.some((node) => node.pos.x === x && node.pos.y === y)) // Not already checked
);
const isInvalidCell = (x, y, width, height, walls) => {
return x >= width // Outside grid boundary
|| x < 0
|| y >= height
|| y < 0
|| walls.some((wall) => wall[0] == x && wall[1] == y) // Collision
}

function aStar(start, end, grid) {
function aStar(start, end, width, height, walls) {
const open = [];
const closed = [];
const out = [];

const startNode = new Node();
startNode.pos = start;
startNode.x = start.x;
startNode.y = start.y;
startNode.g = 0;
startNode.h = 0;
startNode.h = Math.abs(start.x - end.x) + Math.abs(start.y - end.y);
startNode.f = 0;

open.push(startNode)

while (open.length) {
open.sort((a, b) => a.f < b.f); // sort descending by node.f
const q = open.pop();
while (true) {
if (!open.length) {
console.error("No nodes in open list, end not found!");
break;
}

out.push({ pos: q.pos, f: q.f });
open.sort((a, b) => b.f - a.f); // sort descending by node.f
const best = open.pop();

const successors = Object.values(DIRECTIONS)
.map((dir) => {
const successor = new Node();
successor.pos = { x: q.pos.x + dir.x, y: q.pos.y + dir.y };
if (isValid(successor.pos.x, successor.pos.y, closed, grid)) {
successor.g = q.g + 1; // TODO: HOW TO CALC????
const dx = Math.abs(successor.pos.x - end.x)
const dy = Math.abs(successor.pos.y - end.y)
successor.h = (dx + dy) + (Math.sqrt(2) - 2) * Math.min(dx, dy);
successor.f = successor.g + successor.h;
successor.p = q;
return successor;
}
}).filter(Boolean);
if (best.x == end.x && best.y == end.y) {
return best;
}

for (let i = 0; i < successors.length; i++) {
const successor = successors[i];
if (successor.pos.x === end.x && successor.pos.y === end.y) {
closed.push(best);

// --- DEBUG CODE START ---
// grid.map((rows, y) => {
// const row = rows.map((_, x) => {
// const filtered = out.find((node) => node[0].x === x && node[0].y === y)
// return filtered ? Math.ceil(filtered[1]) : 0
// })
// console.log(row)
// }
// );
// --- DEBUG CODE END ---

return out; // stop search
}
if (open.some((node) => node.x === successor.pos.x && node.y === successor.pos.y && node.f < successor.f)) continue; // skip successor
if (closed.some((node) => node.x === successor.pos.x && node.y === successor.pos.y && node.f < successor.f)) continue; // skip successor
open.push(successor);
}
for (dir of Object.values(DIRECTIONS)) {
const x = best.x + dir.x;
const y = best.y + dir.y;

closed.push(q);
if (isInvalidCell(x, y, width, height, walls)) {
continue;
}

const successor = new Node();
successor.x = x;
successor.y = y;
successor.g = best.g + 1;
const dx = Math.abs(x - end.x);
const dy = Math.abs(y - end.y);
// const h = (dx + dy) + (Math.sqrt(2) - 2) * Math.min(dx, dy);
successor.h = dx + dy; // Manhattan Heuristic
successor.f = successor.g + successor.h;
successor.parent = best;

if (!open.some((node) => node.x == successor.x && node.y == successor.y)
&& !closed.some((node) => node.x == successor.x && node.y == successor.y)) {
open.push(successor);
}
}
}
}

Expand Down
48 changes: 30 additions & 18 deletions a-star.test.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
const { aStar } = require('./a-star')
// const { aStar } = require('./a-star')

const TEST_GRID = [
[2,1,1,3],
[0,1,1,0],
[0,1,1,0],
[0,0,0,0]
];
// const TEST_GRID = [
// [2,1,1,3],
// [0,1,1,0],
// [0,1,1,0],
// [0,0,0,0]
// ];

let startPos, endPos;
// let startPos, endPos;

TEST_GRID.map((rows, y) => {
rows.map((cell, x) => {
if (cell === 2) {
startPos = { x, y }
} else if (cell === 3) {
endPos = { x, y }
}
})
})
// TEST_GRID.map((rows, y) => {
// rows.map((cell, x) => {
// if (cell === 2) {
// startPos = { x, y }
// } else if (cell === 3) {
// endPos = { x, y }
// }
// })
// })

aStar(startPos, endPos, TEST_GRID);
// aStar(startPos, endPos, TEST_GRID);

function flattenNodes(node) {
if (node?.pos) {
return [{x: node.pos.x, y: node.pos.y}, ...flattenNodes(node.parent)]
} else {
return []
}
}

const res = flattenNodes({ pos: { x: 0, y: 0 }, parent: { pos: { x: 1, y: 1 }, parent: { pos: { x: 2, y: 2 }}}})

console.log(res);
41 changes: 23 additions & 18 deletions canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,22 @@ function cellToCenteredPixelPos(x, y) {
}

function resetAStarPath() {
[...STATE.aStarPath].forEach(({ pos: { x, y } }) => {
[...STATE.aStarPath].forEach(({ x, y }) => {
if (STATE.grid[y][x] === CELL_STATES.PATH)
STATE.grid[y][x] = CELL_STATES.EMPTY;
});
STATE.aStarPath = [];
}

function drawAStarPath(aStarPath) {
function flattenNodes(node) {
if (!node) return [];
return [{x: node.x, y: node.y}, ...flattenNodes(node.parent)]
}

function drawAStarPath(finalNode) {
const aStarPath = flattenNodes(finalNode);
STATE.aStarPath = aStarPath;
aStarPath.forEach(({ pos: { x, y } }) => {
aStarPath.forEach(({x, y}) => {
if (STATE.grid[y][x] === CELL_STATES.EMPTY)
STATE.grid[y][x] = CELL_STATES.PATH;
});
Expand Down Expand Up @@ -103,29 +109,26 @@ function drawGrid() {
})
}

// function startTimer() {
// TIMER = true;
// const start = Date.now();
// const timerUI = document.getElementById('a-star-timer');
// setTimeout(() => {
// timerUI.innerHTML = Date.now() - start;
// }, 100);
// const end = Date.now();
// timerUI.innerHTML = end - start;
// }

// function endTimer() {
// TIMER = false;
// }
function drawDuration() {
document.getElementById('duration').innerHTML = `${STATE.duration}ms`;
}

function draw() {
resetAStarPath();
if (STATE.startPos && STATE.endPos) {
const result = aStar(STATE.startPos, STATE.endPos, STATE.grid)
const width = STATE.grid[0].length;
const height = STATE.grid.length;

const start = performance.now();
const result = aStar(STATE.startPos, STATE.endPos, width, height, STATE.walls)
const duration = performance.now() - start;
STATE.duration = duration.toFixed(2);

drawAStarPath(result);
}
drawCells();
drawGrid();
drawDuration();
}

function resetCellState() {
Expand Down Expand Up @@ -209,6 +212,8 @@ function setCellState(x, y) {
if (gridState === CELL_STATES.END) break;

// Overwrite EMPTY and PATH states with COLLISION state
const index = STATE.walls.findIndex((wall) => wall[0] == x && wall[1] == y)
index >= 0 ? STATE.walls.splice(index, 1) : STATE.walls.push([x, y]);
STATE.grid[y][x] = [CELL_STATES.EMPTY, CELL_STATES.PATH].includes(gridState)
? CELL_STATES.COLLISION
: CELL_STATES.EMPTY;
Expand Down
6 changes: 3 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ <h1>A* Search Algorithm</h1>
</p>
</header>

<div class="banner banner-warn">
<!-- <div class="banner banner-warn">
<p>This is a work in progress!</p>
<p>
<b>WARNING</b>: It is currently possible to create a path that
Expand All @@ -30,7 +30,7 @@ <h1>A* Search Algorithm</h1>
<li>Creating a room with a single space door where the end point is inside and the start point is outside along the far wall</li>
</ul>
<p><a href="https://timpepper.dev">Take me home!!</a></p>
</div>
</div> -->

<div id="canvas_wrapper">
<div>
Expand Down Expand Up @@ -74,7 +74,7 @@ <h1>A* Search Algorithm</h1>
</div>
</div>
</div>
<!-- <span>Timer: </span><span id="a-star-timer"></span> -->
<span>Timer: </span><span id="duration">0.00ms</span>

<script type="text/javascript" src="./constants.js"></script>
<script type="text/javascript" src="./state.js"></script>
Expand Down
10 changes: 7 additions & 3 deletions state.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ const DEFAULT_STATE = () => ({
cellState: CELL_STATES.COLLISION,
startPos: null,
endPos: null,
aStarPath: []
aStarPath: [],
walls: [],
duration: 0
})

let STATE = {};

const stateInit = () => {
const { grid, cellState, startPos, endPos, aStarPath } = DEFAULT_STATE();
const { grid, cellState, startPos, endPos, aStarPath, walls, duration } = DEFAULT_STATE();
return {
grid,
cellState,
startPos,
endPos,
aStarPath
aStarPath,
walls,
duration
}
};

0 comments on commit 62ef638

Please sign in to comment.