Skip to content

Commit

Permalink
Complete d star lite features and add an example.
Browse files Browse the repository at this point in the history
  • Loading branch information
We-Gold committed Jul 13, 2023
1 parent 6d93157 commit 046cd9a
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 49 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,13 @@ Solutions are simply arrays, with each element as an index in the original `rawM

For example: `[[0,0],[1,0],[1,1],[1,2],...]`

| Name | Description | Method |
| ------------ | ---------------------------------------------------------------------------------------------------------- | ---------------- |
| A\* (A Star) | Fast Dijkstra's-based solver that uses heuristics. Configurable general purpose solver. | `solveAStar` |
| ACO | Ant Colony Optimization is not recommended for real-world purposes. It is interesting for experimentation. | `solveACO` |
| DFS | Depth First Search is simple and fast, exploring deep paths and backtracking. | `solveDFS` |
| BFS | Breadth First Search is fast, simply exploring the whole maze. | `solveBFS` |
| D\* Lite | Essentially A\* backwards. Pretty fast, and ideal for mazes that change that require re-planning. | `solveDStarLite` |
| Name | Description | Method |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| A\* (A Star) | Fast Dijkstra's-based solver that uses heuristics. Configurable general purpose solver. | `solveAStar` |
| ACO | Ant Colony Optimization is not recommended for real-world purposes. It is interesting for experimentation. | `solveACO` |
| DFS | Depth First Search is simple and fast, exploring deep paths and backtracking. | `solveDFS` |
| BFS | Breadth First Search is fast, simply exploring the whole maze. | `solveBFS` |
| D\* Lite | Essentially A\* backwards. Pretty fast, and ideal for mazes that change and require re-planning. _See main.js for examples._ | `solveDStarLite` |

_Many of these algorithms use heuristics or additional configuration. Check of the JSDoc comments for more info._

Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<body>
<main>
<canvas id="demo-canvas" width="400" height="400"></canvas>
<canvas id="demo-canvas-2" width="400" height="400"></canvas>
</main>
<script src="/main.js" type="module"></script>
</body>
Expand Down
7 changes: 2 additions & 5 deletions lib/data-structures/min-heap.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ export const createMinHeap = (

// Decreases the value at an index in the heap
const decreaseKey = (i, newValue) => {
if (array[i] === undefined) {
console.log(keyIndexMap)
console.log(array)
}
array[i].value = newValue

// Maintain the min heap property
Expand All @@ -78,6 +74,7 @@ export const createMinHeap = (

// Modifies the value at an index in the heap
const modifyKey = (i, newValue) => {
const currentKey = array[i].key
const currentValue = array[i].value

// Compare the new value with the current value
Expand All @@ -91,7 +88,7 @@ export const createMinHeap = (
// If the new value is larger, delete the key and reinsert it
else if (valueComparison < 0) {
deleteKey(i)
insert(array[i].key, newValue)
insert(currentKey, newValue)
}
// If the new value is equal to the current value, do nothing
}
Expand Down
4 changes: 4 additions & 0 deletions lib/maze-solving/a-star.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { createMinHeap } from "../data-structures/min-heap"
import { createFilledMatrix, getAvailableNeighbors } from "../helpers"
import { manhattanHeuristic, euclideanHeuristic } from "./heuristics"

/**
* @typedef {import("./heuristics").heuristic} heuristic
*/

/**
* Contains multiple heuristics for the A* algorithm
*/
Expand Down
106 changes: 76 additions & 30 deletions lib/maze-solving/d-star-lite.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
} from "../helpers"
import { manhattanHeuristic, euclideanHeuristic } from "./heuristics"

/**
* @typedef {import("./heuristics").heuristic} heuristic
*/

/**
* Contains multiple heuristics for the D* Lite algorithm
*/
Expand All @@ -15,6 +19,19 @@ export const DStarLiteHeuristic = {
manhattan: manhattanHeuristic,
}

/**
* @typedef DStarLiteResult
* @property {number[][]} path
* @property {solveMethod} solve
*/

/**
* @typedef UpdatedMazeInformation
* @property {number[][]} updatedCellIndices The indices of any updated cells
* @property {number[]} originalCellValues The previous values stored in any updated cells
*/


/**
* Solve the given maze or replan based on changed nodes
* or a different start position.
Expand All @@ -24,16 +41,10 @@ export const DStarLiteHeuristic = {
*
* @callback solveMethod
* @param {number[]} startCell The current cell to plan from
* @param {number[][]} [updatedCellIndices] The indices of any updated cells
* @param {UpdatedMazeInformation} [updatedMazeInformation] Information about any changes to the maze
* @returns {number[][]} The solution path as an array of indices
*/

/**
* @typedef {DStarLiteResult}
* @property {number[][]} path
* @property {solveMethod} solve
*/

/**
* Run D* Lite on a maze, given the start and end positions.
* A custom heuristic can be provided optionally.
Expand Down Expand Up @@ -172,15 +183,6 @@ export const solveDStarLite = (
)

rhs[row][col] = minPotentialRHS

// for (const cellNeighbor of cellNeighbors) {
// const potentialRHS =
// cost(cell, cellNeighbor) +
// g[cellNeighbor[0]][cellNeighbor[1]]

// if (minPotentialRHS > potentialRHS)
// minPotentialRHS = potentialRHS
// }
}
}

Expand All @@ -193,7 +195,10 @@ export const solveDStarLite = (
/**
* @type {solveMethod}
*/
const solve = (startCell, updatedCellIndices = null) => {
const solve = (
startCell,
{ updatedCellIndices, originalCellValues } = {}
) => {
const path = [startCell]
start = startCell
last = start
Expand All @@ -218,19 +223,60 @@ export const solveDStarLite = (
path.push(start)

// Handle any cells that have been modified
// if (updatedCellIndices) {
// kM += h(last, start)
// last = start

// for (const index of updatedCellIndices) {
// for (const cellNeighbor of getAvailableNeighbors(
// index,
// rawMaze
// )) {
// const cOld =
// }
// }
// }
if (updatedCellIndices) {
kM += h(last, start)
last = start

// Consider all cells that have changed
for (const [i, cell] of updatedCellIndices.entries()) {
for (const cellNeighbor of getAvailableNeighbors(
cell,
rawMaze
)) {
const [uRow, uCol] = cellNeighbor

const cNew = cost(cell, cellNeighbor)

// Calculate the old cost
const newCellValue = rawMaze[cell[0]][cell[1]]
rawMaze[cell[0]][cell[1]] = originalCellValues[i]
const cOld = cost(cell, cellNeighbor)
rawMaze[cell[0]][cell[1]] = newCellValue

// Consider this cell as it now has a lower cost than before
if (cOld > cNew)
if (uRow !== goal[0] || uCol !== goal[1])
rhs[uRow][uCol] = Math.min(
rhs[uRow][uCol],
cNew + g[cell[0]][cell[1]]
)
else if (
rhs[uRow][uCol] ==
cOld + g[cell[0]][cell[1]]
) {
if (uRow !== goal[0] || uCol !== goal[1]) {
min = Infinity

for (const _neighbor of getAvailableNeighbors(
cellNeighbor,
rawMaze
)) {
const potential =
cost(cellNeighbor, _neighbor) +
g[_neighbor[0]][_neighbor[1]]

if (min > potential)
min = potential
}

rhs[uRow][uCol] = min
}

updateNode(cellNeighbor)
}
}
}
}

computeShortestPath()
}
Expand Down
62 changes: 56 additions & 6 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ import {
deserializeStringToRaw,
fillWallsWithCells,
solveDStarLite,
structures,
} from "./lib"
import { East, North, South, West, cellIs, removeWall } from "./lib/helpers"

document.addEventListener("DOMContentLoaded", () => {
const canvas = document.getElementById("demo-canvas")
const ctx = canvas.getContext("2d")

const [rows, cols] = [20, 20]
const [rows, cols] = [40, 40]

const start = [0, 0]
const end = [rows - 1, cols - 1]

let startTime = performance.now()
const kruskalMaze = generateMazeKruskal(rows, cols)
Expand All @@ -41,23 +46,25 @@ document.addEventListener("DOMContentLoaded", () => {

const finalMaze = growingTreeMaze

const originalMaze = structuredClone(finalMaze)

// When solving, the hypot (default) heurisitic works well for
// backtracking generated mazes, but the grid heuristic works
// better for kruskal type mazes
startTime = performance.now()
const solution = solveAStar(finalMaze, [0, 0], [19, 19])
const solution = solveAStar(finalMaze, start, end)
endTime = performance.now()

console.log(`A*: ${endTime - startTime}ms`)

startTime = performance.now()
const antSolution = solveACO(finalMaze, [0, 0], [19, 19])
const antSolution = solveACO(finalMaze, start, end)
endTime = performance.now()

console.log(`ACO: ${endTime - startTime}ms`)

startTime = performance.now()
const dStarSolution = solveDStarLite(finalMaze, [0, 0], [19, 19])
const {path: dStarSolution, solve} = solveDStarLite(finalMaze, start, end, true)
endTime = performance.now()

console.log(`D* Lite: ${endTime - startTime}ms`)
Expand All @@ -69,7 +76,7 @@ document.addEventListener("DOMContentLoaded", () => {
console.log(`Node Matrix Conversion: ${endTime - startTime}ms`)

startTime = performance.now()
const nodeGraph = convertRawToNodeGraph(finalMaze, [0, 0], [19, 19])
const nodeGraph = convertRawToNodeGraph(finalMaze, start, end)
endTime = performance.now()

console.log(`Node Graph Conversion: ${endTime - startTime}ms`)
Expand Down Expand Up @@ -104,8 +111,51 @@ document.addEventListener("DOMContentLoaded", () => {
// console.log(filledMaze.map((row) => row.join('')).join('\n'))

startTime = performance.now()
renderMazeToCanvas(ctx, 20, deserialized64, dStarSolution)
renderMazeToCanvas(ctx, 10, originalMaze, dStarSolution)
endTime = performance.now()

console.log(`Render: ${endTime - startTime}ms`)

console.log(structures)

// Render the updated d star path
const canvas2 = document.getElementById("demo-canvas-2")
const ctx2 = canvas2.getContext("2d")

let updatedDStarSolution

const updatedCellIndices = []
const originalCellValues = []

for (let iterations = 0; iterations < 10; iterations++) {
// Randomly remove walls in the maze
for (let i = 1; i < finalMaze.length - 1; i++) {
for (let j = 1; j < finalMaze[0].length - 1; j++) {
if (Math.random() > 0.95) {
updatedCellIndices.push([i, j])

const originalValue = finalMaze[i][j]

originalCellValues.push(originalValue)

if (cellIs(North, originalValue)) {
removeWall([i, j], [i - 1, j], finalMaze)
}
if (cellIs(South, originalValue)) {
removeWall([i, j], [i + 1, j], finalMaze)
}
if (cellIs(East, originalValue)) {
removeWall([i, j], [i, j + 1], finalMaze)
}
if (cellIs(West, originalValue)) {
removeWall([i, j], [i, j - 1], finalMaze)
}
}
}
}

updatedDStarSolution = solve(start, {updatedCellIndices, originalCellValues})
}

renderMazeToCanvas(ctx2, 10, finalMaze, updatedDStarSolution)
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "algernon-js",
"author": "Weaver Goldman <we.goldm@gmail.com>",
"description": "Algernon is a JS library for efficiently generating, solving, and rendering mazes.",
"version": "0.2.2",
"version": "0.2.3",
"type": "module",
"license": "MIT",
"repository": {
Expand Down

0 comments on commit 046cd9a

Please sign in to comment.